refactor: migrate package from com.loki to dev.loki

- Change base package from com.loki to dev.loki across all Java files
- Update plugin.yml main class path
- Fix ConfigParseException imports in test files
- Update build.gradle.kts relocate patterns
- Add missing imports in ConfigLoaderTest.java
- Update TODO.md with current status

BREAKING CHANGE: Package namespace changed from com.loki to dev.loki
This commit is contained in:
loki5512344 2026-05-31 21:34:44 +02:00
parent 41924ea97f
commit f4e9458544
87 changed files with 2185 additions and 1767 deletions

1
.gitignore vendored
View file

@ -36,5 +36,4 @@ Thumbs.db
.kiro/ .kiro/
docs_api/ docs_api/
api_docs/ api_docs/
todo.md
bin/ bin/

View file

@ -44,8 +44,8 @@ dependencies {
compileOnly("net.kyori:adventure-text-minimessage:4.17.0") compileOnly("net.kyori:adventure-text-minimessage:4.17.0")
// Optional plugin integrations (soft dependencies) // Optional plugin integrations (soft dependencies)
compileOnly("com.github.oraxen:oraxen:1.180.0") // compileOnly("com.github.oraxen:oraxen:1.180.0")
compileOnly("com.github.LoneDev6:api-itemsadder:3.6.4") // compileOnly("com.github.LoneDev6:api-itemsadder:3.6.4")
// Test dependencies // Test dependencies
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
@ -63,7 +63,7 @@ tasks {
archiveClassifier.set("") archiveClassifier.set("")
// Relocate LoAPI to avoid conflicts // Relocate LoAPI to avoid conflicts
relocate("dev.lolib", "com.loki.lomines.libs.lolib") relocate("dev.loki.lomines.libs.lolib", "com.loki.lomines.libs.lolib")
// Don't minimize - causes issues with Java 21 // Don't minimize - causes issues with Java 21
// minimize() // minimize()

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

89
gradlew.bat vendored
View file

@ -1,92 +1,7 @@
@rem @rem Gradle startup script for Windows
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
java -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -1,50 +0,0 @@
package com.loki.lomines.integration;
import com.loki.lomines.LoMinesPlugin;
import org.bukkit.Bukkit;
/**
* Manages integrations with external plugins.
* This is a placeholder implementation that will be completed in task 10.1.
*/
public final class IntegrationManager {
private final LoMinesPlugin plugin;
public IntegrationManager(LoMinesPlugin plugin) {
this.plugin = plugin;
}
/**
* Initializes all available integrations.
*/
public void initAll() {
// Check for PlaceholderAPI
if (isPluginEnabled("PlaceholderAPI")) {
plugin.loLogger().info("PlaceholderAPI integration available");
}
// Check for WorldGuard
if (isPluginEnabled("WorldGuard")) {
plugin.loLogger().info("WorldGuard integration available");
}
// Check for Oraxen
if (isPluginEnabled("Oraxen")) {
plugin.loLogger().info("Oraxen integration available");
}
// Check for ItemsAdder
if (isPluginEnabled("ItemsAdder")) {
plugin.loLogger().info("ItemsAdder integration available");
}
}
/**
* Checks if a plugin is enabled.
*/
public boolean isPluginEnabled(String pluginName) {
return Bukkit.getPluginManager().getPlugin(pluginName) != null &&
Bukkit.getPluginManager().isPluginEnabled(pluginName);
}
}

View file

@ -1,10 +1,10 @@
package com.loki.lomines; package dev.loki.lomines;
import com.loki.lomines.core.MineTicker; import dev.loki.lomines.core.MineTicker;
import com.loki.lomines.core.Mines; import dev.loki.lomines.core.Mines;
import com.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.data.stats.StatsManager;
import com.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.IntegrationManager;
import com.loki.lomines.wand.GroupWandManager; import dev.loki.lomines.wand.GroupWandManager;
import dev.lolib.commands.CommandManager; import dev.lolib.commands.CommandManager;
import java.io.IOException; import java.io.IOException;
@ -73,10 +73,11 @@ final class ComponentInitializer {
} }
record Components( record Components(
Mines mines, Mines mines,
GroupWandManager groupWandManager, GroupWandManager groupWandManager,
StatsManager statsManager, StatsManager statsManager,
CommandManager commandManager, CommandManager commandManager,
IntegrationManager integrationManager IntegrationManager integrationManager
) {} ) {
}
} }

View file

@ -1,10 +1,10 @@
package com.loki.lomines; package dev.loki.lomines;
import com.loki.lomines.core.MineTicker; import dev.loki.lomines.core.MineTicker;
import com.loki.lomines.core.Mines; import dev.loki.lomines.core.Mines;
import com.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.data.stats.StatsManager;
import com.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.IntegrationManager;
import com.loki.lomines.wand.GroupWandManager; import dev.loki.lomines.wand.GroupWandManager;
import dev.lolib.commands.CommandManager; import dev.lolib.commands.CommandManager;
import dev.lolib.core.LoPlugin; import dev.lolib.core.LoPlugin;
import dev.lolib.core.dependency.DependencyManager; import dev.lolib.core.dependency.DependencyManager;
@ -73,6 +73,10 @@ public final class LoMinesPlugin extends LoPlugin {
loLogger().info("Statistics saved successfully"); loLogger().info("Statistics saved successfully");
} }
if (integrationManager != null) {
integrationManager.shutdown();
}
loLogger().info("LoMines has been disabled!"); loLogger().info("LoMines has been disabled!");
} catch (Exception e) { } catch (Exception e) {
loLogger().error("Error during plugin shutdown: " + e.getMessage()); loLogger().error("Error during plugin shutdown: " + e.getMessage());

View file

@ -1,14 +1,14 @@
package com.loki.lomines; package dev.loki.lomines;
import com.loki.lomines.command.AdminCommands; import dev.loki.lomines.command.AdminCommands;
import com.loki.lomines.command.MaskCommands; import dev.loki.lomines.command.MaskCommands;
import com.loki.lomines.command.PlayerCommands; import dev.loki.lomines.command.PlayerCommands;
import com.loki.lomines.command.StatsCommands; import dev.loki.lomines.command.StatsCommands;
import com.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.IntegrationManager;
import com.loki.lomines.listener.BlockBreakListener; import dev.loki.lomines.listener.BlockBreakListener;
import com.loki.lomines.listener.GroupGuiListener; import dev.loki.lomines.listener.GroupGuiListener;
import com.loki.lomines.listener.PlayerInteractListener; import dev.loki.lomines.listener.PlayerInteractListener;
import com.loki.lomines.listener.PlayerJoinListener; import dev.loki.lomines.listener.PlayerJoinListener;
import dev.lolib.commands.CommandManager; import dev.lolib.commands.CommandManager;
/** /**

View file

@ -1,6 +1,6 @@
package com.loki.lomines.block; package dev.loki.lomines.block;
import com.loki.lomines.util.Cuboid; import dev.loki.lomines.util.location.Cuboid;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.List; import java.util.List;
@ -25,7 +25,7 @@ public abstract class BlockSetter {
* Fills a cuboid region with blocks asynchronously. * Fills a cuboid region with blocks asynchronously.
* The callback is invoked in the main thread after completion. * The callback is invoked in the main thread after completion.
* *
* @param region The region to fill * @param region The region to fill
* @param callback Called with the number of blocks set when complete * @param callback Called with the number of blocks set when complete
*/ */
public abstract void fill(Cuboid region, IntConsumer callback); public abstract void fill(Cuboid region, IntConsumer callback);

View file

@ -1,16 +1,15 @@
package com.loki.lomines.block; package dev.loki.lomines.block;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.util.ValidationUtils;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.Scheduler;
import com.loki.lomines.LoMinesPlugin;
import com.loki.lomines.util.Cuboid;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.Location;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.IntConsumer; import java.util.function.IntConsumer;
@ -92,14 +91,14 @@ public final class BukkitBlockSetter extends BlockSetter {
currentWeight += entry.getValue(); currentWeight += entry.getValue();
if (random <= currentWeight) { if (random <= currentWeight) {
// Convert string key to BlockData // Convert string key to BlockData
Material material = Material.valueOf(entry.getKey().toUpperCase()); Material material = ValidationUtils.parseMaterialOrDefault(entry.getKey(), Material.STONE);
return Bukkit.createBlockData(material); return Bukkit.createBlockData(material);
} }
} }
// Fallback to first entry // Fallback to first entry
String firstKey = weights.keySet().iterator().next(); String firstKey = weights.keySet().iterator().next();
Material material = Material.valueOf(firstKey.toUpperCase()); Material material = ValidationUtils.parseMaterialOrDefault(firstKey, Material.STONE);
return Bukkit.createBlockData(material); return Bukkit.createBlockData(material);
} }
} }

View file

@ -9,4 +9,4 @@
* <li>{@code ItemsAdderBlockSetter} - Implementation for ItemsAdder custom blocks</li> * <li>{@code ItemsAdderBlockSetter} - Implementation for ItemsAdder custom blocks</li>
* </ul> * </ul>
*/ */
package com.loki.lomines.block; package dev.loki.lomines.block;

View file

@ -1,8 +1,10 @@
package com.loki.lomines.command; package dev.loki.lomines.command;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.core.Mine; import dev.loki.lomines.core.Mine;
import com.loki.lomines.core.Mines; import dev.loki.lomines.core.Mines;
import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.util.MessageFormatter;
import dev.lolib.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Command; import dev.lolib.commands.annotation.Command;
import dev.lolib.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
@ -21,10 +23,12 @@ public class AdminCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Mines mines; private final Mines mines;
private final ErrorHandler errorHandler;
public AdminCommands(LoMinesPlugin plugin) { public AdminCommands(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.mines = plugin.getMines(); this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
} }
/** /**
@ -35,15 +39,15 @@ public class AdminCommands {
public void create(CommandSender sender, @Arg("name") String name) { public void create(CommandSender sender, @Arg("name") String name) {
try { try {
mines.create(name); mines.create(name);
sender.sendMessage(Component.text("Successfully created mine: " + name)); sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name));
sender.sendMessage(Component.text( sender.sendMessage(Component.text(
"Edit selection & contents in plugins/LoMines/mines/" + name + ".yml — " "Edit selection & contents in plugins/LoMines/mines/" + name + ".yml — "
+ "for shape-based ore (not full cuboid), paint mask.marker blocks, then: /lm maskscan " + name)); + "for shape-based ore (not full cuboid), paint mask.marker blocks, then: /lm maskscan " + name));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
sender.sendMessage(Component.text("Mine already exists: " + name)); errorHandler.handleNotFound(sender, "Mine already exists", name);
} catch (IOException e) { } catch (IOException e) {
sender.sendMessage(Component.text("Failed to create mine: " + e.getMessage())); errorHandler.handleError(sender, "Failed to create mine: " + e.getMessage(),
plugin.loLogger().error("Failed to create mine " + name, e); "Failed to create mine " + name, e);
} }
} }
@ -55,12 +59,12 @@ public class AdminCommands {
public void delete(CommandSender sender, @Arg("name") String name) { public void delete(CommandSender sender, @Arg("name") String name) {
try { try {
mines.delete(name); mines.delete(name);
sender.sendMessage(Component.text("Successfully deleted mine: " + name)); sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
sender.sendMessage(Component.text("Mine not found: " + name)); errorHandler.handleNotFound(sender, "Mine", name);
} catch (IOException e) { } catch (IOException e) {
sender.sendMessage(Component.text("Failed to delete mine: " + e.getMessage())); errorHandler.handleError(sender, "Failed to delete mine: " + e.getMessage(),
plugin.loLogger().error("Failed to delete mine " + name, e); "Failed to delete mine " + name, e);
} }
} }
@ -70,16 +74,16 @@ public class AdminCommands {
*/ */
@Subcommand(value = "reset", permission = "lomines.admin.reset") @Subcommand(value = "reset", permission = "lomines.admin.reset")
public void reset( public void reset(
CommandSender sender, CommandSender sender,
@Arg("mine") String mineName, @Arg("mine") String mineName,
@Arg(value = "silent", optional = true, defaultValue = "false") boolean silent @Arg(value = "silent", optional = true, defaultValue = "false") boolean silent
) { ) {
try { try {
Mine mine = mines.get(mineName); Mine mine = mines.get(mineName);
mine.reset(silent); mine.reset(silent);
sender.sendMessage(Component.text("Resetting mine: " + mineName)); sender.sendMessage(MessageFormatter.success("Resetting mine: " + mineName));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
sender.sendMessage(Component.text("Mine not found: " + mineName)); errorHandler.handleNotFound(sender, "Mine", mineName);
} }
} }
@ -90,17 +94,13 @@ public class AdminCommands {
@Subcommand(value = "reload", permission = "lomines.admin.reload") @Subcommand(value = "reload", permission = "lomines.admin.reload")
public void reload(CommandSender sender) { public void reload(CommandSender sender) {
try { try {
// Stop all existing mines
mines.getAll().forEach(Mine::stop); mines.getAll().forEach(Mine::stop);
// Reload all mines
mines.loadAll(); mines.loadAll();
int count = mines.getAll().size(); int count = mines.getAll().size();
sender.sendMessage(Component.text("Successfully reloaded " + count + " mine(s)")); sender.sendMessage(MessageFormatter.success("Successfully reloaded " + count + " mine(s)"));
} catch (IOException e) { } catch (IOException e) {
sender.sendMessage(Component.text("Failed to reload mines: " + e.getMessage())); errorHandler.handleError(sender, "Failed to reload mines: " + e.getMessage(),
plugin.loLogger().error("Failed to reload mines", e); "Failed to reload mines", e);
} }
} }

View file

@ -1,13 +1,14 @@
package com.loki.lomines.command; package dev.loki.lomines.command;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.core.Mines; import dev.loki.lomines.core.Mines;
import com.loki.lomines.data.config.ConfigParseException; import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.util.MessageFormatter;
import dev.lolib.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Command; import dev.lolib.commands.annotation.Command;
import dev.lolib.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.Scheduler;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import java.io.IOException; import java.io.IOException;
@ -21,29 +22,27 @@ public class MaskCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Mines mines; private final Mines mines;
private final ErrorHandler errorHandler;
public MaskCommands(LoMinesPlugin plugin) { public MaskCommands(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.mines = plugin.getMines(); this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
} }
/**
* Scans selection for marker blocks ({@code mask.marker}, default pink concrete), saves mask positions and switches mine to mask fill mode.
* Run from the server main thread (scheduled automatically).
*/
@Subcommand(value = "maskscan", permission = "lomines.admin.maskscan") @Subcommand(value = "maskscan", permission = "lomines.admin.maskscan")
public void maskScan(CommandSender sender, @Arg("mine") String name) { public void maskScan(CommandSender sender, @Arg("mine") String name) {
Scheduler.get(plugin).run(() -> { Scheduler.get(plugin).run(() -> {
try { try {
int n = mines.scanAndSaveMask(name); int n = mines.scanAndSaveMask(name);
sender.sendMessage(Component.text( sender.sendMessage(MessageFormatter.success(
"Mask mode: saved " + n + " cell(s) for mine '" + name "Mask mode: saved " + n + " cell(s) for mine '" + name
+ "'. Reset will spawn ore only at those positions.")); + "'. Reset will spawn ore only at those positions."));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
sender.sendMessage(Component.text("Mine not found: " + name)); errorHandler.handleNotFound(sender, "Mine", name);
} catch (IOException | ConfigParseException e) { } catch (IOException | ConfigParseException e) {
sender.sendMessage(Component.text("Mask scan failed: " + e.getMessage())); errorHandler.handleError(sender, "Mask scan failed: " + e.getMessage(),
plugin.loLogger().error("maskscan failed for " + name, e); "maskscan failed for " + name, e);
} }
}); });
} }

View file

@ -1,7 +1,8 @@
package com.loki.lomines.command; package dev.loki.lomines.command;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.wand.GroupWandItem; import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.wand.GroupWandItem;
import dev.lolib.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Command; import dev.lolib.commands.annotation.Command;
import dev.lolib.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
@ -18,27 +19,29 @@ import org.bukkit.entity.Player;
public class PlayerCommands { public class PlayerCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final ErrorHandler errorHandler;
public PlayerCommands(LoMinesPlugin plugin) { public PlayerCommands(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
} }
@Subcommand(value = "wand", permission = "lomines.admin.wand") @Subcommand(value = "wand", permission = "lomines.admin.wand")
public void wand(CommandSender sender) { public void wand(CommandSender sender) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Только для игрока.")); errorHandler.sendError(sender, "Только для игрока.");
return; return;
} }
player.getInventory().addItem(GroupWandItem.create(plugin)); player.getInventory().addItem(GroupWandItem.create(plugin));
player.sendMessage(Component.text( player.sendMessage(Component.text(
"Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>",
NamedTextColor.GREEN)); NamedTextColor.GREEN));
} }
@Subcommand(value = "group", permission = "lomines.admin.wand") @Subcommand(value = "group", permission = "lomines.admin.wand")
public void group(CommandSender sender, @Arg("prefix") String prefix) { public void group(CommandSender sender, @Arg("prefix") String prefix) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Префикс задаётся в игре.")); errorHandler.sendError(sender, "Префикс задаётся в игре.");
return; return;
} }
plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix); plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix);

View file

@ -1,12 +1,13 @@
package com.loki.lomines.command; package dev.loki.lomines.command;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.core.Mine; import dev.loki.lomines.core.Mine;
import com.loki.lomines.core.Mines; import dev.loki.lomines.core.Mines;
import com.loki.lomines.data.Leaderboard; import dev.loki.lomines.data.stats.Leaderboard;
import com.loki.lomines.data.LeaderboardEntry; import dev.loki.lomines.data.stats.LeaderboardEntry;
import com.loki.lomines.data.PlayerStats; import dev.loki.lomines.data.stats.PlayerStats;
import com.loki.lomines.data.StatsManager; import dev.loki.lomines.data.stats.StatsManager;
import dev.loki.lomines.util.ErrorHandler;
import dev.lolib.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Command; import dev.lolib.commands.annotation.Command;
import dev.lolib.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
@ -30,10 +31,44 @@ public class StatsCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Mines mines; private final Mines mines;
private final ErrorHandler errorHandler;
public StatsCommands(LoMinesPlugin plugin) { public StatsCommands(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.mines = plugin.getMines(); 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;
}
} }
/** /**
@ -49,13 +84,13 @@ public class StatsCommands {
UUID uuid; UUID uuid;
if (name.isEmpty()) { if (name.isEmpty()) {
if (!(sender instanceof Player self)) { if (!(sender instanceof Player self)) {
sender.sendMessage(Component.text("Укажите игрока: /lm stats <ник>", NamedTextColor.RED)); errorHandler.sendError(sender, "Укажите игрока: /lm stats <ник>");
return; return;
} }
uuid = self.getUniqueId(); uuid = self.getUniqueId();
} else { } else {
if (!sender.hasPermission("lomines.stats.others")) { if (!sender.hasPermission("lomines.stats.others")) {
sender.sendMessage(Component.text("Нет прав смотреть чужую статистику.", NamedTextColor.RED)); errorHandler.handlePermissionDenied(sender, "смотреть чужую статистику");
return; return;
} }
Player online = Bukkit.getPlayerExact(name); Player online = Bukkit.getPlayerExact(name);
@ -81,10 +116,10 @@ public class StatsCommands {
} }
sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY)); sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY));
byMine.entrySet().stream() byMine.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed()) .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(12) .limit(12)
.forEach(e -> sender.sendMessage(Component.text( .forEach(e -> sender.sendMessage(Component.text(
" " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA))); " " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA)));
} }
/** /**
@ -94,9 +129,9 @@ public class StatsCommands {
*/ */
@Subcommand(value = "top", permission = "lomines.stats") @Subcommand(value = "top", permission = "lomines.stats")
public void top( public void top(
CommandSender sender, CommandSender sender,
@Arg(value = "arg1", optional = true, defaultValue = "") String arg1, @Arg(value = "arg1", optional = true, defaultValue = "") String arg1,
@Arg(value = "arg2", optional = true, defaultValue = "") String arg2 @Arg(value = "arg2", optional = true, defaultValue = "") String arg2
) { ) {
if (isStatsDisabled(sender)) { if (isStatsDisabled(sender)) {
return; return;
@ -126,7 +161,7 @@ public class StatsCommands {
if (mineName != null) { if (mineName != null) {
var found = mines.find(mineName); var found = mines.find(mineName);
if (found.isEmpty()) { if (found.isEmpty()) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); errorHandler.handleNotFound(sender, "Шахта", mineName);
return; return;
} }
Mine m = found.get(); Mine m = found.get();
@ -141,25 +176,6 @@ public class StatsCommands {
sendTopRows(sender, rows); sendTopRows(sender, rows);
} }
private static void sendTopRows(CommandSender sender, List<LeaderboardEntry> rows) {
if (rows.isEmpty()) {
sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY));
return;
}
for (int i = 0; i < rows.size(); i++) {
LeaderboardEntry e = rows.get(i);
String name = formatPlayerName(e.getPlayerId());
sender.sendMessage(Component.text(
(i + 1) + ". " + name + " — " + e.getCount(),
NamedTextColor.WHITE));
}
}
private static String formatPlayerName(UUID id) {
String name = Bukkit.getOfflinePlayer(id).getName();
return name != null ? name : id.toString();
}
private boolean isStatsDisabled(CommandSender sender) { private boolean isStatsDisabled(CommandSender sender) {
if (!plugin.getConfig().getBoolean("statistics-enabled", true)) { if (!plugin.getConfig().getBoolean("statistics-enabled", true)) {
sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED)); sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED));
@ -167,17 +183,4 @@ public class StatsCommands {
} }
return false; return false;
} }
private static int clampLimit(int limit) {
return Math.max(1, Math.min(50, limit));
}
private static Integer tryParsePositiveInt(String s) {
try {
int v = Integer.parseInt(s);
return v > 0 ? v : null;
} catch (NumberFormatException e) {
return null;
}
}
} }

View file

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

View file

@ -1,26 +1,21 @@
package com.loki.lomines.core; package dev.loki.lomines.core;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.block.BlockSetter; import dev.loki.lomines.block.BlockSetter;
import com.loki.lomines.data.FillMode; import dev.loki.lomines.data.config.FillMode;
import com.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import com.loki.lomines.util.BlockKeys; import dev.loki.lomines.handler.ActionBarHandler;
import com.loki.lomines.handler.ActionBarHandler; import dev.loki.lomines.handler.MineBlockHandler;
import com.loki.lomines.handler.MineBlockHandler; import dev.loki.lomines.handler.MineResetHandler;
import com.loki.lomines.handler.MineResetHandler; import dev.loki.lomines.util.location.BlockKeys;
import com.loki.lomines.util.Cuboid; import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.scheduler.Scheduler;
import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
@ -36,7 +31,9 @@ public final class Mine {
private final BlockSetter blockSetter; private final BlockSetter blockSetter;
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final int totalVolume; private final int totalVolume;
/** Block keys for {@link FillMode#MASK}; empty when not mask mode. */ /**
* Block keys for {@link FillMode#MASK}; empty when not mask mode.
*/
private final Set<String> maskBlockKeys; private final Set<String> maskBlockKeys;
// Mutable state (thread-safe) // Mutable state (thread-safe)
@ -186,11 +183,13 @@ public final class Mine {
String firstKey = blocks.keySet().iterator().next(); String firstKey = blocks.keySet().iterator().next();
if (firstKey.startsWith("oraxen:")) { if (firstKey.startsWith("oraxen:")) {
return new com.loki.lomines.block.OraxenBlockSetter(blocks, plugin); // return new dev.loki.lomines.block.OraxenBlockSetter(blocks, plugin);
throw new IllegalArgumentException("Oraxen integration is currently disabled");
} else if (firstKey.startsWith("itemsadder:")) { } else if (firstKey.startsWith("itemsadder:")) {
return new com.loki.lomines.block.ItemsAdderBlockSetter(blocks, plugin); // return new dev.loki.lomines.block.ItemsAdderBlockSetter(blocks, plugin);
throw new IllegalArgumentException("ItemsAdder integration is currently disabled");
} else { } else {
return new com.loki.lomines.block.BukkitBlockSetter(blocks, plugin); return new dev.loki.lomines.block.BukkitBlockSetter(blocks, plugin);
} }
} }
@ -207,12 +206,35 @@ public final class Mine {
}, 10L, 10L); }, 10L, 10L);
} }
public String getName() { return name; } public String getName() {
public MineConfig getConfig() { return config; } return name;
public BlockSetter getBlockSetter() { return blockSetter; } }
public List<Cuboid> getRegions() { return regions; }
public AtomicInteger getBlocksAtomic() { return blocks; } public MineConfig getConfig() {
public int getBlocks() { return blocks.get(); } return config;
public int getTicks() { return ticks.get(); } }
public AtomicInteger getTicksAtomic() { return ticks; }
public BlockSetter getBlockSetter() {
return blockSetter;
}
public List<Cuboid> getRegions() {
return regions;
}
public AtomicInteger getBlocksAtomic() {
return blocks;
}
public int getBlocks() {
return blocks.get();
}
public int getTicks() {
return ticks.get();
}
public AtomicInteger getTicksAtomic() {
return ticks;
}
} }

View file

@ -1,8 +1,8 @@
package com.loki.lomines.core; package dev.loki.lomines.core;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.scheduler.Scheduler;
import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
/** /**
* Centralized ticker for all mines. * Centralized ticker for all mines.

View file

@ -1,8 +1,11 @@
package com.loki.lomines.core; package dev.loki.lomines.core;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.data.config.ConfigParseException; import dev.loki.lomines.core.service.MaskScanService;
import com.loki.lomines.data.config.MineConfig; import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.core.service.MineRepository;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.Location; import org.bukkit.Location;
import java.io.IOException; import java.io.IOException;
@ -45,7 +48,7 @@ public final class Mines {
try (Stream<Path> paths = Files.list(minesFolder)) { try (Stream<Path> paths = Files.list(minesFolder)) {
paths.filter(path -> path.toString().endsWith(".yml")) paths.filter(path -> path.toString().endsWith(".yml"))
.forEach(configFile -> loadMineFromFile(configFile)); .forEach(configFile -> loadMineFromFile(configFile));
} }
} }
@ -111,6 +114,7 @@ public final class Mines {
/** /**
* Gets a mine by name. * Gets a mine by name.
*
* @throws IllegalArgumentException if mine not found * @throws IllegalArgumentException if mine not found
*/ */
public Mine get(String name) { public Mine get(String name) {

View file

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

View file

@ -1,8 +1,10 @@
package com.loki.lomines.core; package dev.loki.lomines.core.service;
import com.loki.lomines.data.config.ConfigParseException; import dev.loki.lomines.core.Mine;
import com.loki.lomines.util.location.Cuboid; import dev.loki.lomines.data.config.parser.ConfigParseException;
import com.loki.lomines.util.selection.MaskScanner; import dev.loki.lomines.util.ValidationUtils;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.selection.MaskScanner;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
@ -12,17 +14,17 @@ import java.util.List;
/** /**
* Service for scanning and saving mask positions. * Service for scanning and saving mask positions.
*/ */
final class MaskScanService { public final class MaskScanService {
private final MineRepository repository; private final MineRepository repository;
private final MineFileManager fileManager; private final MineFileManager fileManager;
MaskScanService(MineRepository repository, MineFileManager fileManager) { public MaskScanService(MineRepository repository, MineFileManager fileManager) {
this.repository = repository; this.repository = repository;
this.fileManager = fileManager; this.fileManager = fileManager;
} }
int scanAndSave(String mineName) throws IOException, ConfigParseException { public int scanAndSave(String mineName) throws IOException, ConfigParseException {
Mine mine = repository.get(mineName); Mine mine = repository.get(mineName);
Material marker = parseMarkerMaterial(mine.getConfig().getMaskMarkerMaterial()); Material marker = parseMarkerMaterial(mine.getConfig().getMaskMarkerMaterial());
@ -30,9 +32,9 @@ final class MaskScanService {
List<Location> found = MaskScanner.scan(regions, marker); List<Location> found = MaskScanner.scan(regions, marker);
fileManager.saveMaskPositions( fileManager.saveMaskPositions(
mineName, mineName,
mine.getConfig().getMaskMarkerMaterial(), mine.getConfig().getMaskMarkerMaterial(),
found found
); );
repository.reload(mineName); repository.reload(mineName);
@ -41,10 +43,6 @@ final class MaskScanService {
} }
private Material parseMarkerMaterial(String materialName) { private Material parseMarkerMaterial(String materialName) {
try { return ValidationUtils.parseMaterialOrDefault(materialName, Material.PINK_CONCRETE);
return Material.valueOf(materialName.toUpperCase());
} catch (IllegalArgumentException e) {
return Material.PINK_CONCRETE;
}
} }
} }

View file

@ -1,9 +1,9 @@
package com.loki.lomines.core; package dev.loki.lomines.core.service;
import com.loki.lomines.data.config.ConfigLoader; import dev.loki.lomines.data.config.ConfigLoader;
import com.loki.lomines.data.config.ConfigParseException; import dev.loki.lomines.data.config.MineConfig;
import com.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.parser.ConfigParseException;
import com.loki.lomines.util.location.LocationParser; import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -17,21 +17,19 @@ import java.util.Map;
/** /**
* Handles file operations for mine configurations. * Handles file operations for mine configurations.
*/ */
final class MineFileManager { public record MineFileManager(Path minesFolder) {
private final Path minesFolder; public Path getMinesFolder() {
return minesFolder;
MineFileManager(Path minesFolder) {
this.minesFolder = minesFolder;
} }
void ensureFolderExists() throws IOException { public void ensureFolderExists() throws IOException {
if (!Files.exists(minesFolder)) { if (!Files.exists(minesFolder)) {
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
} }
} }
void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException { public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException {
Path configFile = minesFolder.resolve(name + ".yml"); Path configFile = minesFolder.resolve(name + ".yml");
YamlConfiguration yaml = new YamlConfiguration(); YamlConfiguration yaml = new YamlConfiguration();
@ -62,7 +60,7 @@ final class MineFileManager {
yaml.save(configFile.toFile()); yaml.save(configFile.toFile());
} }
MineConfig loadConfig(String name) throws IOException, ConfigParseException { public MineConfig loadConfig(String name) throws IOException, ConfigParseException {
Path configFile = minesFolder.resolve(name + ".yml"); Path configFile = minesFolder.resolve(name + ".yml");
if (!Files.exists(configFile)) { if (!Files.exists(configFile)) {
throw new IOException("Mine config not found: " + configFile); throw new IOException("Mine config not found: " + configFile);
@ -71,14 +69,14 @@ final class MineFileManager {
return loader.load(configFile); return loader.load(configFile);
} }
void deleteConfig(String name) throws IOException { public void deleteConfig(String name) throws IOException {
Path configFile = minesFolder.resolve(name + ".yml"); Path configFile = minesFolder.resolve(name + ".yml");
if (Files.exists(configFile)) { if (Files.exists(configFile)) {
Files.delete(configFile); Files.delete(configFile);
} }
} }
void saveMaskPositions(String name, String markerMaterial, List<Location> positions) throws IOException { public void saveMaskPositions(String name, String markerMaterial, List<Location> positions) throws IOException {
Path configFile = minesFolder.resolve(name + ".yml"); Path configFile = minesFolder.resolve(name + ".yml");
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configFile.toFile()); YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configFile.toFile());
@ -93,8 +91,4 @@ final class MineFileManager {
yaml.save(configFile.toFile()); yaml.save(configFile.toFile());
} }
Path getMinesFolder() {
return minesFolder;
}
} }

View file

@ -1,8 +1,9 @@
package com.loki.lomines.core; package dev.loki.lomines.core.service;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.data.config.ConfigParseException; import dev.loki.lomines.core.Mine;
import com.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.Location; import org.bukkit.Location;
import java.io.IOException; import java.io.IOException;
@ -12,26 +13,26 @@ import java.util.concurrent.ConcurrentHashMap;
/** /**
* Repository for managing mine instances. * Repository for managing mine instances.
*/ */
final class MineRepository { public final class MineRepository {
private final Map<String, Mine> mines = new ConcurrentHashMap<>(); private final Map<String, Mine> mines = new ConcurrentHashMap<>();
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final MineFileManager fileManager; private final MineFileManager fileManager;
MineRepository(LoMinesPlugin plugin, MineFileManager fileManager) { public MineRepository(LoMinesPlugin plugin, MineFileManager fileManager) {
this.plugin = plugin; this.plugin = plugin;
this.fileManager = fileManager; this.fileManager = fileManager;
} }
void add(String name, Mine mine) { public void add(String name, Mine mine) {
mines.put(name.toLowerCase(), mine); mines.put(name.toLowerCase(), mine);
} }
Mine remove(String name) { public Mine remove(String name) {
return mines.remove(name.toLowerCase()); return mines.remove(name.toLowerCase());
} }
Mine get(String name) { public Mine get(String name) {
Mine mine = mines.get(name.toLowerCase()); Mine mine = mines.get(name.toLowerCase());
if (mine == null) { if (mine == null) {
throw new IllegalArgumentException("Mine not found: " + name); throw new IllegalArgumentException("Mine not found: " + name);
@ -39,46 +40,46 @@ final class MineRepository {
return mine; return mine;
} }
Optional<Mine> find(String name) { public Optional<Mine> find(String name) {
return Optional.ofNullable(mines.get(name.toLowerCase())); return Optional.ofNullable(mines.get(name.toLowerCase()));
} }
Collection<Mine> getAll() { public Collection<Mine> getAll() {
return Collections.unmodifiableCollection(mines.values()); return Collections.unmodifiableCollection(mines.values());
} }
Optional<Mine> findByLocation(Location location) { public Optional<Mine> findByLocation(Location location) {
return mines.values().stream() return mines.values().stream()
.filter(mine -> mine.contains(location)) .filter(mine -> mine.contains(location))
.findFirst(); .findFirst();
} }
boolean exists(String name) { public boolean exists(String name) {
return mines.containsKey(name.toLowerCase()); return mines.containsKey(name.toLowerCase());
} }
void clear() { public void clear() {
for (Mine mine : new ArrayList<>(mines.values())) { for (Mine mine : new ArrayList<>(mines.values())) {
mine.stop(); mine.stop();
} }
mines.clear(); mines.clear();
} }
Mine createAndStart(String name, MineConfig config) { public Mine createAndStart(String name, MineConfig config) {
Mine mine = new Mine(name, config, plugin); Mine mine = new Mine(name, config, plugin);
add(name, mine); add(name, mine);
mine.start(); mine.start();
return mine; return mine;
} }
void stopAndRemove(String name) { public void stopAndRemove(String name) {
Mine mine = remove(name); Mine mine = remove(name);
if (mine != null) { if (mine != null) {
mine.stop(); mine.stop();
} }
} }
void reload(String name) throws IOException, ConfigParseException { public void reload(String name) throws IOException, ConfigParseException {
stopAndRemove(name); stopAndRemove(name);
MineConfig config = fileManager.loadConfig(name); MineConfig config = fileManager.loadConfig(name);
Mine mine = createAndStart(name, config); Mine mine = createAndStart(name, config);

View file

@ -1,5 +1,9 @@
package com.loki.lomines.data.config; package dev.loki.lomines.data.config;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.data.config.parser.ConfigParser;
import dev.loki.lomines.data.reward.Reward;
import dev.loki.lomines.data.reward.RewardParser;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -32,7 +36,7 @@ public final class ConfigLoader {
* *
* @param configFile the path to the YAML configuration file * @param configFile the path to the YAML configuration file
* @return the loaded MineConfig object * @return the loaded MineConfig object
* @throws IOException if the file cannot be read * @throws IOException if the file cannot be read
* @throws ConfigParseException if the configuration is invalid * @throws ConfigParseException if the configuration is invalid
*/ */
public MineConfig load(Path configFile) throws IOException, ConfigParseException { public MineConfig load(Path configFile) throws IOException, ConfigParseException {
@ -63,33 +67,33 @@ public final class ConfigLoader {
// Build and return config // Build and return config
return MineConfig.builder() return MineConfig.builder()
.selections(selections) .selections(selections)
.blocks(blocks) .blocks(blocks)
.fillMode(fillMode) .fillMode(fillMode)
.maskMarkerMaterial(maskMarker) .maskMarkerMaterial(maskMarker)
.maskPositions(maskPositions) .maskPositions(maskPositions)
.rewards(rewards) .rewards(rewards)
.resetTicks(yaml.getInt("reset.ticks", 6000)) .resetTicks(yaml.getInt("reset.ticks", 6000))
.resetPercent(yaml.getDouble("reset.percent", 10.0)) .resetPercent(yaml.getDouble("reset.percent", 10.0))
.resetOnPercentEnabled(yaml.getBoolean("reset-on-percent", false)) .resetOnPercentEnabled(yaml.getBoolean("reset-on-percent", false))
.actionBarEnabled(yaml.getBoolean("actionbar.enabled", true)) .actionBarEnabled(yaml.getBoolean("actionbar.enabled", true))
.actionBarMessage(yaml.getString("actionbar.message", "")) .actionBarMessage(yaml.getString("actionbar.message", ""))
.actionBarRange(yaml.getDouble("actionbar.range", 50.0)) .actionBarRange(yaml.getDouble("actionbar.range", 50.0))
.timerFormat(yaml.getString("timer-format", "mm:ss")) .timerFormat(yaml.getString("timer-format", "mm:ss"))
.teleportOnReset(yaml.getBoolean("teleport-on-reset", false)) .teleportOnReset(yaml.getBoolean("teleport-on-reset", false))
.teleportLocation(teleportLocation) .teleportLocation(teleportLocation)
.resetCommands(yaml.getStringList("reset-commands")) .resetCommands(yaml.getStringList("reset-commands"))
.broadcastReset(yaml.getString("broadcast-reset", "")) .broadcastReset(yaml.getString("broadcast-reset", ""))
.build(); .build();
} }
/** /**
* Saves a MineConfig to a YAML file. * Saves a MineConfig to a YAML file.
* Serializes the config and writes it to the specified file. * Serializes the config and writes it to the specified file.
* *
* @param config the MineConfig to save * @param config the MineConfig to save
* @param configFile the path to the YAML configuration file * @param configFile the path to the YAML configuration file
* @throws IOException if the file cannot be written * @throws IOException if the file cannot be written
* @throws IllegalArgumentException if config or configFile is null * @throws IllegalArgumentException if config or configFile is null
*/ */
public void save(MineConfig config, Path configFile) throws IOException { public void save(MineConfig config, Path configFile) throws IOException {

View file

@ -1,16 +1,13 @@
package com.loki.lomines.data.config; package dev.loki.lomines.data.config;
import com.loki.lomines.util.LocationParser; import dev.loki.lomines.data.reward.Reward;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** /**
* Serializes MineConfig objects to YAML format. * Serializes MineConfig objects to YAML format.
@ -22,7 +19,7 @@ final class ConfigSerializer {
* Serializes a MineConfig to YAML configuration. * Serializes a MineConfig to YAML configuration.
* *
* @param config the MineConfig to serialize * @param config the MineConfig to serialize
* @param yaml the YAML configuration to write to * @param yaml the YAML configuration to write to
*/ */
void serialize(MineConfig config, YamlConfiguration yaml) { void serialize(MineConfig config, YamlConfiguration yaml) {
saveSelections(yaml, config.getSelections()); saveSelections(yaml, config.getSelections());
@ -53,7 +50,7 @@ final class ConfigSerializer {
/** /**
* Saves selection coordinates to the YAML configuration. * Saves selection coordinates to the YAML configuration.
* *
* @param yaml the YAML configuration * @param yaml the YAML configuration
* @param selections the list of selection locations * @param selections the list of selection locations
*/ */
private void saveSelections(YamlConfiguration yaml, List<Location> selections) { private void saveSelections(YamlConfiguration yaml, List<Location> selections) {
@ -67,7 +64,7 @@ final class ConfigSerializer {
/** /**
* Saves block contents to the YAML configuration. * Saves block contents to the YAML configuration.
* *
* @param yaml the YAML configuration * @param yaml the YAML configuration
* @param blocks the map of block string key to weight * @param blocks the map of block string key to weight
*/ */
private void saveBlocks(YamlConfiguration yaml, Map<String, Double> blocks) { private void saveBlocks(YamlConfiguration yaml, Map<String, Double> blocks) {
@ -81,7 +78,7 @@ final class ConfigSerializer {
/** /**
* Saves mask positions to the YAML configuration. * Saves mask positions to the YAML configuration.
* *
* @param yaml the YAML configuration * @param yaml the YAML configuration
* @param maskPositions the list of mask positions * @param maskPositions the list of mask positions
*/ */
private void saveMaskPositions(YamlConfiguration yaml, List<Location> maskPositions) { private void saveMaskPositions(YamlConfiguration yaml, List<Location> maskPositions) {
@ -100,7 +97,7 @@ final class ConfigSerializer {
/** /**
* Saves rewards to the YAML configuration. * Saves rewards to the YAML configuration.
* *
* @param yaml the YAML configuration * @param yaml the YAML configuration
* @param rewards the list of rewards * @param rewards the list of rewards
*/ */
private void saveRewards(YamlConfiguration yaml, List<Reward> rewards) { private void saveRewards(YamlConfiguration yaml, List<Reward> rewards) {

View file

@ -1,7 +1,9 @@
package com.loki.lomines.data.config; package dev.loki.lomines.data.config;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.data.reward.Reward;
import dev.loki.lomines.util.ValidationUtils;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -21,14 +23,14 @@ final class ConfigValidator {
void validateSelections(List<Location> selections) throws ConfigParseException { void validateSelections(List<Location> selections) throws ConfigParseException {
if (selections.isEmpty()) { if (selections.isEmpty()) {
throw new ConfigParseException( throw new ConfigParseException(
"No selection coordinates found. At least one selection pair (2 points) is required" "No selection coordinates found. At least one selection pair (2 points) is required"
); );
} }
if (selections.size() % 2 != 0) { if (selections.size() % 2 != 0) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid number of selection coordinates: " + selections.size() + "Invalid number of selection coordinates: " + selections.size() +
". Selections must be in pairs (even number)" ". Selections must be in pairs (even number)"
); );
} }
} }
@ -42,7 +44,7 @@ final class ConfigValidator {
void validateBlocks(Map<String, Double> blocks) throws ConfigParseException { void validateBlocks(Map<String, Double> blocks) throws ConfigParseException {
if (blocks.isEmpty()) { if (blocks.isEmpty()) {
throw new ConfigParseException( throw new ConfigParseException(
"No blocks defined in 'contents' section. At least one block is required" "No blocks defined in 'contents' section. At least one block is required"
); );
} }
@ -52,7 +54,7 @@ final class ConfigValidator {
if (weight <= 0) { if (weight <= 0) {
throw new ConfigParseException( throw new ConfigParseException(
"Block weight must be positive for '" + key + "', got: " + weight "Block weight must be positive for '" + key + "', got: " + weight
); );
} }
@ -71,11 +73,11 @@ final class ConfigValidator {
*/ */
void validateMaterial(String materialName) throws ConfigParseException { void validateMaterial(String materialName) throws ConfigParseException {
try { try {
Material.valueOf(materialName.toUpperCase()); ValidationUtils.validateMaterial(materialName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Unknown material: '" + materialName + "'. Must be a valid Minecraft material name", "Unknown material: '" + materialName + "'. Must be a valid Minecraft material name",
e e
); );
} }
} }
@ -88,7 +90,7 @@ final class ConfigValidator {
*/ */
void validateMaskMarker(String marker) throws ConfigParseException { void validateMaskMarker(String marker) throws ConfigParseException {
try { try {
Material.valueOf(marker.toUpperCase()); ValidationUtils.validateMaterial(marker);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new ConfigParseException("Invalid mask.marker material: '" + marker + "'", e); throw new ConfigParseException("Invalid mask.marker material: '" + marker + "'", e);
} }
@ -104,7 +106,7 @@ final class ConfigValidator {
double chance = reward.getChance(); double chance = reward.getChance();
if (chance < 0 || chance > 100) { if (chance < 0 || chance > 100) {
throw new ConfigParseException( throw new ConfigParseException(
"Reward chance must be between 0 and 100, got: " + chance "Reward chance must be between 0 and 100, got: " + chance
); );
} }
@ -116,7 +118,7 @@ final class ConfigValidator {
for (org.bukkit.inventory.ItemStack item : reward.getItems()) { for (org.bukkit.inventory.ItemStack item : reward.getItems()) {
if (item.getAmount() <= 0) { if (item.getAmount() <= 0) {
throw new ConfigParseException( throw new ConfigParseException(
"Item amount must be positive, got: " + item.getAmount() "Item amount must be positive, got: " + item.getAmount()
); );
} }
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.data; package dev.loki.lomines.data.config;
/** /**
* How mine contents are placed on reset. * How mine contents are placed on reset.

View file

@ -1,5 +1,6 @@
package com.loki.lomines.data.config; package dev.loki.lomines.data.config;
import dev.loki.lomines.data.reward.Reward;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.List; import java.util.List;
@ -19,11 +20,17 @@ public final class MineConfig {
// Blocks // Blocks
private final Map<String, Double> blocks; private final Map<String, Double> blocks;
/** How blocks are placed on reset. */ /**
* How blocks are placed on reset.
*/
private final FillMode fillMode; private final FillMode fillMode;
/** Vanilla material name for {@link FillMode#MASK} scan (e.g. pink_concrete). */ /**
* Vanilla material name for {@link FillMode#MASK} scan (e.g. pink_concrete).
*/
private final String maskMarkerMaterial; private final String maskMarkerMaterial;
/** Block centers for mask mode; ignored when {@link #fillMode} is {@link FillMode#CUBOID}. */ /**
* Block centers for mask mode; ignored when {@link #fillMode} is {@link FillMode#CUBOID}.
*/
private final List<Location> maskPositions; private final List<Location> maskPositions;
// Rewards // Rewards

View file

@ -1,5 +1,6 @@
package com.loki.lomines.data.config; package dev.loki.lomines.data.config;
import dev.loki.lomines.data.reward.Reward;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.ArrayList; import java.util.ArrayList;
@ -119,10 +120,10 @@ public final class MineConfigBuilder {
public MineConfig build() { public MineConfig build() {
return new MineConfig( return new MineConfig(
selections, blocks, fillMode, maskMarkerMaterial, maskPositions, selections, blocks, fillMode, maskMarkerMaterial, maskPositions,
rewards, resetTicks, resetPercent, resetOnPercentEnabled, rewards, resetTicks, resetPercent, resetOnPercentEnabled,
resetCommands, broadcastReset, teleportOnReset, teleportLocation, resetCommands, broadcastReset, teleportOnReset, teleportLocation,
actionBarEnabled, actionBarMessage, actionBarRange, timerFormat actionBarEnabled, actionBarMessage, actionBarRange, timerFormat
); );
} }
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.data.config; package dev.loki.lomines.data.config.parser;
/** /**
* Exception thrown when configuration parsing fails. * Exception thrown when configuration parsing fails.

View file

@ -1,26 +1,25 @@
package com.loki.lomines.data.config; package dev.loki.lomines.data.config.parser;
import com.loki.lomines.util.LocationParser; import dev.loki.lomines.data.config.FillMode;
import dev.loki.lomines.data.reward.Reward;
import dev.loki.lomines.data.reward.RewardParser;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** /**
* Parses YAML configuration into intermediate data structures. * Parses YAML configuration into intermediate data structures.
* Responsible only for reading YAML and converting to Java objects. * Responsible only for reading YAML and converting to Java objects.
* Does not perform validation - that's handled by ConfigValidator. * Does not perform validation - that's handled by ConfigValidator.
*/ */
final class ConfigParser { public final class ConfigParser {
private final RewardParser rewardParser; private final RewardParser rewardParser;
ConfigParser(RewardParser rewardParser) { public ConfigParser(RewardParser rewardParser) {
this.rewardParser = rewardParser; this.rewardParser = rewardParser;
} }
@ -32,7 +31,7 @@ final class ConfigParser {
* @return list of parsed Location objects * @return list of parsed Location objects
* @throws ConfigParseException if location parsing fails * @throws ConfigParseException if location parsing fails
*/ */
List<Location> parseSelections(YamlConfiguration yaml) throws ConfigParseException { public List<Location> parseSelections(YamlConfiguration yaml) throws ConfigParseException {
List<Location> selections = new ArrayList<>(); List<Location> selections = new ArrayList<>();
for (int i = 1; i <= 10; i++) { for (int i = 1; i <= 10; i++) {
@ -41,7 +40,7 @@ final class ConfigParser {
String locationString = yaml.getString(key); String locationString = yaml.getString(key);
if (locationString == null || locationString.trim().isEmpty()) { if (locationString == null || locationString.trim().isEmpty()) {
throw new ConfigParseException( throw new ConfigParseException(
"Selection coordinate at '" + key + "' is empty" "Selection coordinate at '" + key + "' is empty"
); );
} }
@ -50,8 +49,8 @@ final class ConfigParser {
selections.add(location); selections.add(location);
} catch (ConfigParseException e) { } catch (ConfigParseException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid selection coordinate at '" + key + "': " + e.getMessage(), "Invalid selection coordinate at '" + key + "': " + e.getMessage(),
e e
); );
} }
} }
@ -68,12 +67,12 @@ final class ConfigParser {
* @return map of block string key to weight * @return map of block string key to weight
* @throws ConfigParseException if block configuration is invalid * @throws ConfigParseException if block configuration is invalid
*/ */
Map<String, Double> parseBlocks(YamlConfiguration yaml) throws ConfigParseException { public Map<String, Double> parseBlocks(YamlConfiguration yaml) throws ConfigParseException {
ConfigurationSection contentsSection = yaml.getConfigurationSection("contents"); ConfigurationSection contentsSection = yaml.getConfigurationSection("contents");
if (contentsSection == null) { if (contentsSection == null) {
throw new ConfigParseException( throw new ConfigParseException(
"Missing required 'contents' section in configuration" "Missing required 'contents' section in configuration"
); );
} }
@ -98,7 +97,7 @@ final class ConfigParser {
* @param yaml the YAML configuration * @param yaml the YAML configuration
* @return the parsed FillMode * @return the parsed FillMode
*/ */
FillMode parseFillMode(YamlConfiguration yaml) { public FillMode parseFillMode(YamlConfiguration yaml) {
String raw = yaml.getString("fill-mode", "cuboid"); String raw = yaml.getString("fill-mode", "cuboid");
if (raw == null || raw.isBlank()) { if (raw == null || raw.isBlank()) {
return FillMode.CUBOID; return FillMode.CUBOID;
@ -115,7 +114,7 @@ final class ConfigParser {
* @param yaml the YAML configuration * @param yaml the YAML configuration
* @return the mask marker material name * @return the mask marker material name
*/ */
String parseMaskMarker(YamlConfiguration yaml) { public String parseMaskMarker(YamlConfiguration yaml) {
String marker = yaml.getString("mask.marker", "pink_concrete"); String marker = yaml.getString("mask.marker", "pink_concrete");
if (marker == null || marker.isBlank()) { if (marker == null || marker.isBlank()) {
return "pink_concrete"; return "pink_concrete";
@ -130,7 +129,7 @@ final class ConfigParser {
* @return list of mask positions * @return list of mask positions
* @throws ConfigParseException if position parsing fails * @throws ConfigParseException if position parsing fails
*/ */
List<Location> parseMaskPositions(YamlConfiguration yaml) throws ConfigParseException { public List<Location> parseMaskPositions(YamlConfiguration yaml) throws ConfigParseException {
List<String> rawList = yaml.getStringList("mask.positions"); List<String> rawList = yaml.getStringList("mask.positions");
if (rawList == null || rawList.isEmpty()) { if (rawList == null || rawList.isEmpty()) {
return List.of(); return List.of();
@ -146,8 +145,8 @@ final class ConfigParser {
out.add(LocationParser.parse(line.trim())); out.add(LocationParser.parse(line.trim()));
} catch (ConfigParseException e) { } catch (ConfigParseException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid mask.positions entry at index " + i + ": " + e.getMessage(), "Invalid mask.positions entry at index " + i + ": " + e.getMessage(),
e e
); );
} }
} }
@ -162,7 +161,7 @@ final class ConfigParser {
* @return list of parsed Reward objects * @return list of parsed Reward objects
* @throws ConfigParseException if reward configuration is invalid * @throws ConfigParseException if reward configuration is invalid
*/ */
List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException { public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
return rewardParser.parseRewards(yaml); return rewardParser.parseRewards(yaml);
} }
@ -173,7 +172,7 @@ final class ConfigParser {
* @return the parsed Location, or null if not configured * @return the parsed Location, or null if not configured
* @throws ConfigParseException if the teleport location is invalid * @throws ConfigParseException if the teleport location is invalid
*/ */
Location parseTeleportLocation(YamlConfiguration yaml) throws ConfigParseException { public Location parseTeleportLocation(YamlConfiguration yaml) throws ConfigParseException {
if (!yaml.contains("teleport-location")) { if (!yaml.contains("teleport-location")) {
return null; return null;
} }
@ -187,8 +186,8 @@ final class ConfigParser {
return LocationParser.parse(locationString); return LocationParser.parse(locationString);
} catch (ConfigParseException e) { } catch (ConfigParseException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid teleport location: " + e.getMessage(), "Invalid teleport location: " + e.getMessage(),
e e
); );
} }
} }
@ -201,14 +200,14 @@ final class ConfigParser {
return Double.parseDouble((String) value); return Double.parseDouble((String) value);
} else { } else {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid weight type for block '" + key + "': expected number, got " + "Invalid weight type for block '" + key + "': expected number, got " +
(value != null ? value.getClass().getSimpleName() : "null") (value != null ? value.getClass().getSimpleName() : "null")
); );
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid weight value for block '" + key + "': " + value, "Invalid weight value for block '" + key + "': " + value,
e e
); );
} }
} }

View file

@ -12,4 +12,4 @@
* <li>{@code ConfigParseException} - Exception for configuration parsing errors</li> * <li>{@code ConfigParseException} - Exception for configuration parsing errors</li>
* </ul> * </ul>
*/ */
package com.loki.lomines.data; package dev.loki.lomines.data;

View file

@ -1,4 +1,4 @@
package com.loki.lomines.data.reward; package dev.loki.lomines.data.reward;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@ -10,13 +10,8 @@ import java.util.Objects;
* Immutable reward configuration for mine blocks. * Immutable reward configuration for mine blocks.
* All collections are copied to ensure immutability. * All collections are copied to ensure immutability.
*/ */
public final class Reward { public record Reward(List<Material> materials, double chance, List<ItemStack> items, List<String> commands,
private final List<Material> materials; boolean preventDrops) {
private final double chance;
private final List<ItemStack> items;
private final List<String> commands;
private final boolean preventDrops;
public Reward(List<Material> materials, double chance, public Reward(List<Material> materials, double chance,
List<ItemStack> items, List<String> commands, List<ItemStack> items, List<String> commands,
boolean preventDrops) { boolean preventDrops) {
@ -31,6 +26,7 @@ public final class Reward {
return materials.contains(material); return materials.contains(material);
} }
// JavaBean-style getters for compatibility
public List<Material> getMaterials() { public List<Material> getMaterials() {
return materials; return materials;
} }
@ -63,8 +59,4 @@ public final class Reward {
Objects.equals(commands, reward.commands); Objects.equals(commands, reward.commands);
} }
@Override
public int hashCode() {
return Objects.hash(materials, chance, items, commands, preventDrops);
}
} }

View file

@ -1,4 +1,7 @@
package com.loki.lomines.data.reward; package dev.loki.lomines.data.reward;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@ -15,7 +18,7 @@ final class RewardItemParser {
* Parses a single reward item from a map configuration. * Parses a single reward item from a map configuration.
* *
* @param itemMap the item configuration map * @param itemMap the item configuration map
* @param index the index of the item in the list (for error messages) * @param index the index of the item in the list (for error messages)
* @return the parsed ItemStack * @return the parsed ItemStack
* @throws ConfigParseException if the item configuration is invalid * @throws ConfigParseException if the item configuration is invalid
*/ */
@ -40,11 +43,11 @@ final class RewardItemParser {
*/ */
private Material parseMaterial(String typeName) throws ConfigParseException { private Material parseMaterial(String typeName) throws ConfigParseException {
try { try {
return Material.valueOf(typeName.toUpperCase()); return ValidationUtils.validateMaterial(typeName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Unknown material in item type: '" + typeName + "'", "Unknown material in item type: '" + typeName + "'",
e e
); );
} }
} }
@ -69,8 +72,8 @@ final class RewardItemParser {
return Integer.parseInt((String) amountObj); return Integer.parseInt((String) amountObj);
} else { } else {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid 'amount' type: expected number, got " + "Invalid 'amount' type: expected number, got " +
(amountObj != null ? amountObj.getClass().getSimpleName() : "null") (amountObj != null ? amountObj.getClass().getSimpleName() : "null")
); );
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {

View file

@ -1,4 +1,7 @@
package com.loki.lomines.data.reward; package dev.loki.lomines.data.reward;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -12,11 +15,11 @@ import java.util.Map;
* Parses reward configurations from YAML. * Parses reward configurations from YAML.
* Handles reward chance, materials, items, commands, and prevent-drops settings. * Handles reward chance, materials, items, commands, and prevent-drops settings.
*/ */
final class RewardParser { public final class RewardParser {
private final RewardItemParser itemParser; private final RewardItemParser itemParser;
RewardParser() { public RewardParser() {
this.itemParser = new RewardItemParser(); this.itemParser = new RewardItemParser();
} }
@ -27,7 +30,7 @@ final class RewardParser {
* @return list of parsed Reward objects * @return list of parsed Reward objects
* @throws ConfigParseException if reward configuration is invalid * @throws ConfigParseException if reward configuration is invalid
*/ */
List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException { public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
List<Reward> rewards = new ArrayList<>(); List<Reward> rewards = new ArrayList<>();
if (!yaml.contains("random-rewards")) { if (!yaml.contains("random-rewards")) {
@ -44,8 +47,8 @@ final class RewardParser {
if (!(rewardObj instanceof Map)) { if (!(rewardObj instanceof Map)) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid reward at index " + i + ": expected map, got " + "Invalid reward at index " + i + ": expected map, got " +
(rewardObj != null ? rewardObj.getClass().getSimpleName() : "null") (rewardObj != null ? rewardObj.getClass().getSimpleName() : "null")
); );
} }
@ -57,8 +60,8 @@ final class RewardParser {
rewards.add(reward); rewards.add(reward);
} catch (ConfigParseException e) { } catch (ConfigParseException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Error parsing reward at index " + i + ": " + e.getMessage(), "Error parsing reward at index " + i + ": " + e.getMessage(),
e e
); );
} }
} }
@ -96,8 +99,8 @@ final class RewardParser {
} else if (type == Boolean.class) { } else if (type == Boolean.class) {
if (!(value instanceof Boolean)) { if (!(value instanceof Boolean)) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid '" + key + "' value: expected boolean, got " + "Invalid '" + key + "' value: expected boolean, got " +
(value != null ? value.getClass().getSimpleName() : "null") (value != null ? value.getClass().getSimpleName() : "null")
); );
} }
return (T) value; return (T) value;
@ -125,8 +128,8 @@ final class RewardParser {
return Double.parseDouble((String) value); return Double.parseDouble((String) value);
} else { } else {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid '" + key + "' type: expected number, got " + "Invalid '" + key + "' type: expected number, got " +
(value != null ? value.getClass().getSimpleName() : "null") (value != null ? value.getClass().getSimpleName() : "null")
); );
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
@ -142,8 +145,8 @@ final class RewardParser {
Object blocksObj = rewardMap.get("blocks"); Object blocksObj = rewardMap.get("blocks");
if (!(blocksObj instanceof List)) { if (!(blocksObj instanceof List)) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid 'blocks' type: expected list, got " + "Invalid 'blocks' type: expected list, got " +
(blocksObj != null ? blocksObj.getClass().getSimpleName() : "null") (blocksObj != null ? blocksObj.getClass().getSimpleName() : "null")
); );
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -154,12 +157,12 @@ final class RewardParser {
throw new ConfigParseException("Block name cannot be null or empty"); throw new ConfigParseException("Block name cannot be null or empty");
} }
try { try {
Material material = Material.valueOf(blockName.toUpperCase()); Material material = ValidationUtils.validateMaterial(blockName);
materials.add(material); materials.add(material);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Unknown material in reward blocks: '" + blockName + "'", "Unknown material in reward blocks: '" + blockName + "'",
e e
); );
} }
} }
@ -173,19 +176,18 @@ final class RewardParser {
return items; return items;
} }
Object itemsObj = rewardMap.get("items"); Object itemsObj = rewardMap.get("items");
if (!(itemsObj instanceof List)) { if (!(itemsObj instanceof List<?> itemsList)) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid 'items' type: expected list, got " + "Invalid 'items' type: expected list, got " +
(itemsObj != null ? itemsObj.getClass().getSimpleName() : "null") (itemsObj != null ? itemsObj.getClass().getSimpleName() : "null")
); );
} }
List<?> itemsList = (List<?>) itemsObj;
for (int i = 0; i < itemsList.size(); i++) { for (int i = 0; i < itemsList.size(); i++) {
Object itemObj = itemsList.get(i); Object itemObj = itemsList.get(i);
if (!(itemObj instanceof Map)) { if (!(itemObj instanceof Map)) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid item at index " + i + ": expected map, got " + "Invalid item at index " + i + ": expected map, got " +
(itemObj != null ? itemObj.getClass().getSimpleName() : "null") (itemObj != null ? itemObj.getClass().getSimpleName() : "null")
); );
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -205,8 +207,8 @@ final class RewardParser {
Object commandsObj = rewardMap.get("commands"); Object commandsObj = rewardMap.get("commands");
if (!(commandsObj instanceof List)) { if (!(commandsObj instanceof List)) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid 'commands' type: expected list, got " + "Invalid 'commands' type: expected list, got " +
(commandsObj != null ? commandsObj.getClass().getSimpleName() : "null") (commandsObj != null ? commandsObj.getClass().getSimpleName() : "null")
); );
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View file

@ -1,4 +1,4 @@
package com.loki.lomines.data.stats; package dev.loki.lomines.data.stats;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@ -8,17 +8,16 @@ import java.util.stream.Collectors;
/** /**
* Manages leaderboard functionality with caching. * Manages leaderboard functionality with caching.
* Provides methods to retrieve top players by total blocks or by specific mine. * Provides methods to retrieve top players by total blocks or by specific mine.
* * <p>
* Thread-safety: Uses volatile cache and synchronized blocks for thread-safe operations. * Thread-safety: Uses volatile cache and synchronized blocks for thread-safe operations.
* Cache is invalidated when player statistics change. * Cache is invalidated when player statistics change.
*/ */
public final class Leaderboard { public final class Leaderboard {
private final StatsManager statsManager; private final StatsManager statsManager;
private final Object cacheLock = new Object();
// Cache for getTopTotal - volatile for visibility across threads // Cache for getTopTotal - volatile for visibility across threads
private volatile List<LeaderboardEntry> cachedTopTotal = null; private volatile List<LeaderboardEntry> cachedTopTotal = null;
private final Object cacheLock = new Object();
/** /**
* Creates a new Leaderboard instance. * Creates a new Leaderboard instance.
@ -61,10 +60,10 @@ public final class Leaderboard {
// Build the cache // Build the cache
cachedTopTotal = statsManager.getAllStats().stream() cachedTopTotal = statsManager.getAllStats().stream()
.filter(stats -> stats.getTotalBlocks() > 0) .filter(stats -> stats.getTotalBlocks() > 0)
.sorted(Comparator.comparingLong(PlayerStats::getTotalBlocks).reversed()) .sorted(Comparator.comparingLong(PlayerStats::getTotalBlocks).reversed())
.map(stats -> new LeaderboardEntry(stats.getPlayerId(), stats.getTotalBlocks())) .map(stats -> new LeaderboardEntry(stats.getPlayerId(), stats.getTotalBlocks()))
.collect(Collectors.toList()); .collect(Collectors.toList());
return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size())); return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size()));
} }
@ -75,16 +74,16 @@ public final class Leaderboard {
* This method does not use caching as mine-specific leaderboards are less frequently accessed. * This method does not use caching as mine-specific leaderboards are less frequently accessed.
* *
* @param mineName The name of the mine * @param mineName The name of the mine
* @param limit Maximum number of entries to return * @param limit Maximum number of entries to return
* @return List of leaderboard entries sorted by descending block count for the specified mine * @return List of leaderboard entries sorted by descending block count for the specified mine
*/ */
public List<LeaderboardEntry> getTopByMine(String mineName, int limit) { public List<LeaderboardEntry> getTopByMine(String mineName, int limit) {
return statsManager.getAllStats().stream() return statsManager.getAllStats().stream()
.map(stats -> new LeaderboardEntry(stats.getPlayerId(), stats.getMineBlocks(mineName))) .map(stats -> new LeaderboardEntry(stats.getPlayerId(), stats.getMineBlocks(mineName)))
.filter(entry -> entry.getCount() > 0) .filter(entry -> entry.count() > 0)
.sorted(Comparator.comparingLong(LeaderboardEntry::getCount).reversed()) .sorted(Comparator.comparingLong(LeaderboardEntry::count).reversed())
.limit(limit) .limit(limit)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/** /**
@ -96,7 +95,7 @@ public final class Leaderboard {
public int getPosition(UUID playerId) { public int getPosition(UUID playerId) {
List<LeaderboardEntry> top = getTopTotal(Integer.MAX_VALUE); List<LeaderboardEntry> top = getTopTotal(Integer.MAX_VALUE);
for (int i = 0; i < top.size(); i++) { for (int i = 0; i < top.size(); i++) {
if (top.get(i).getPlayerId().equals(playerId)) { if (top.get(i).playerId().equals(playerId)) {
return i + 1; // 1-based position return i + 1; // 1-based position
} }
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.data.stats; package dev.loki.lomines.data.stats;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@ -7,24 +7,13 @@ import java.util.UUID;
* Immutable data class representing a single entry in the leaderboard. * Immutable data class representing a single entry in the leaderboard.
* Contains a player's UUID and their block count. * Contains a player's UUID and their block count.
*/ */
public final class LeaderboardEntry { public record LeaderboardEntry(UUID playerId, long count) {
private final UUID playerId;
private final long count;
public LeaderboardEntry(UUID playerId, long count) { public LeaderboardEntry(UUID playerId, long count) {
this.playerId = Objects.requireNonNull(playerId, "playerId cannot be null"); this.playerId = Objects.requireNonNull(playerId, "playerId cannot be null");
this.count = count; this.count = count;
} }
public UUID getPlayerId() {
return playerId;
}
public long getCount() {
return count;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -33,11 +22,6 @@ public final class LeaderboardEntry {
return count == that.count && playerId.equals(that.playerId); return count == that.count && playerId.equals(that.playerId);
} }
@Override
public int hashCode() {
return Objects.hash(playerId, count);
}
@Override @Override
public String toString() { public String toString() {
return "LeaderboardEntry{playerId=" + playerId + ", count=" + count + "}"; return "LeaderboardEntry{playerId=" + playerId + ", count=" + count + "}";

View file

@ -1,4 +1,4 @@
package com.loki.lomines.data.stats; package dev.loki.lomines.data.stats;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -26,6 +26,10 @@ public final class PlayerStats {
return totalBlocks.get(); return totalBlocks.get();
} }
public void setTotalBlocks(long value) {
totalBlocks.set(value);
}
public long getMineBlocks(String mineName) { public long getMineBlocks(String mineName) {
AtomicLong counter = mineStats.get(mineName.toLowerCase()); AtomicLong counter = mineStats.get(mineName.toLowerCase());
return counter != null ? counter.get() : 0; return counter != null ? counter.get() : 0;
@ -37,16 +41,12 @@ public final class PlayerStats {
public void incrementMine(String mineName) { public void incrementMine(String mineName) {
mineStats.computeIfAbsent(mineName.toLowerCase(), k -> new AtomicLong(0)) mineStats.computeIfAbsent(mineName.toLowerCase(), k -> new AtomicLong(0))
.incrementAndGet(); .incrementAndGet();
}
public void setTotalBlocks(long value) {
totalBlocks.set(value);
} }
public void setMineBlocks(String mineName, long value) { public void setMineBlocks(String mineName, long value) {
mineStats.computeIfAbsent(mineName.toLowerCase(), k -> new AtomicLong(0)) mineStats.computeIfAbsent(mineName.toLowerCase(), k -> new AtomicLong(0))
.set(value); .set(value);
} }
public java.util.Map<String, Long> getMineStatsSnapshot() { public java.util.Map<String, Long> getMineStatsSnapshot() {

View file

@ -1,8 +1,8 @@
package com.loki.lomines.data.stats; package dev.loki.lomines.data.stats;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.scheduler.Scheduler;
import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -123,16 +123,16 @@ public final class StatsManager {
long interval = 20L * 60L * 5L; long interval = 20L * 60L * 5L;
saveTask = Scheduler.get(plugin).runTimerAsync( saveTask = Scheduler.get(plugin).runTimerAsync(
() -> { () -> {
try { try {
save(); save();
plugin.loLogger().info("Statistics auto-saved"); plugin.loLogger().info("Statistics auto-saved");
} catch (IOException e) { } catch (IOException e) {
plugin.loLogger().error("Failed to auto-save statistics: " + e.getMessage()); plugin.loLogger().error("Failed to auto-save statistics: " + e.getMessage());
} }
}, },
interval, interval,
interval interval
); );
} }

View file

@ -1,9 +1,9 @@
package com.loki.lomines.gui; package dev.loki.lomines.gui;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.core.Mines; import dev.loki.lomines.core.Mines;
import com.loki.lomines.wand.GroupWandManager; import dev.loki.lomines.wand.GroupWandManager;
import com.loki.lomines.wand.GroupWandSession; import dev.loki.lomines.wand.GroupWandSession;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@ -21,12 +21,18 @@ import java.util.List;
*/ */
public final class GroupCreateGui { public final class GroupCreateGui {
private static final int SIZE = 54; /**
/** Create group (same YAML defaults for every created mine). */ * Create group (same YAML defaults for every created mine).
*/
public static final int SLOT_CREATE = 0; public static final int SLOT_CREATE = 0;
/** Shows /lm group prefix. */ /**
* Shows /lm group prefix.
*/
public static final int SLOT_INFO = 8; public static final int SLOT_INFO = 8;
/** 3×3 mine slots mapping index 0..8 */ private static final int SIZE = 54;
/**
* 3×3 mine slots mapping index 0..8
*/
private static final int[] MINE_SLOTS = {10, 11, 12, 19, 20, 21, 28, 29, 30}; private static final int[] MINE_SLOTS = {10, 11, 12, 19, 20, 21, 28, 29, 30};
private GroupCreateGui() { private GroupCreateGui() {
@ -35,7 +41,7 @@ public final class GroupCreateGui {
public static void open(LoMinesPlugin plugin, Player player) { public static void open(LoMinesPlugin plugin, Player player) {
GroupCreateGuiHolder holder = new GroupCreateGuiHolder(player.getUniqueId()); GroupCreateGuiHolder holder = new GroupCreateGuiHolder(player.getUniqueId());
Inventory inv = Bukkit.createInventory(holder, SIZE, Inventory inv = Bukkit.createInventory(holder, SIZE,
Component.text("LoMines · группа (9 шахт)", NamedTextColor.DARK_GREEN)); Component.text("LoMines · группа (9 шахт)", NamedTextColor.DARK_GREEN));
holder.setInventory(inv); holder.setInventory(inv);
fill(plugin, player, inv); fill(plugin, player, inv);
player.openInventory(inv); player.openInventory(inv);
@ -73,8 +79,8 @@ public final class GroupCreateGui {
if (meta != null) { if (meta != null) {
meta.displayName(Component.text("Создать группу", NamedTextColor.GREEN)); meta.displayName(Component.text("Создать группу", NamedTextColor.GREEN));
meta.lore(List.of( meta.lore(List.of(
Component.text("Создаёт шахты с префиксом_1 … _9", NamedTextColor.GRAY), Component.text("Создаёт шахты с префиксом_1 … _9", NamedTextColor.GRAY),
Component.text("для слотов, где заданы оба угла", NamedTextColor.GRAY) Component.text("для слотов, где заданы оба угла", NamedTextColor.GRAY)
)); ));
stack.setItemMeta(meta); stack.setItemMeta(meta);
} }
@ -88,8 +94,8 @@ public final class GroupCreateGui {
if (session.hasBaseName()) { if (session.hasBaseName()) {
meta.displayName(Component.text("Префикс: " + session.getBaseName(), NamedTextColor.AQUA)); meta.displayName(Component.text("Префикс: " + session.getBaseName(), NamedTextColor.AQUA));
meta.lore(List.of( meta.lore(List.of(
Component.text("Файлы: " + session.getBaseName() + "_1.yml … _9.yml", NamedTextColor.GRAY), Component.text("Файлы: " + session.getBaseName() + "_1.yml … _9.yml", NamedTextColor.GRAY),
Component.text("Настройки одинаковые; правьте один и копируйте при нужде", NamedTextColor.DARK_GRAY) Component.text("Настройки одинаковые; правьте один и копируйте при нужде", NamedTextColor.DARK_GRAY)
)); ));
} else { } else {
meta.displayName(Component.text("Задайте префикс", NamedTextColor.RED)); meta.displayName(Component.text("Задайте префикс", NamedTextColor.RED));
@ -126,8 +132,8 @@ public final class GroupCreateGui {
List<Component> lore = new ArrayList<>(); List<Component> lore = new ArrayList<>();
lore.add(Component.text(active ? "Активный слот палочки" : "ПКМ — выбрать активным", NamedTextColor.GRAY)); lore.add(Component.text(active ? "Активный слот палочки" : "ПКМ — выбрать активным", NamedTextColor.GRAY));
lore.add(Component.text( lore.add(Component.text(
ready ? "Готово (2 точки)" : (session.getPos1(index) != null ? "Нужна 2-я точка" : "Нужны 2 точки"), ready ? "Готово (2 точки)" : (session.getPos1(index) != null ? "Нужна 2-я точка" : "Нужны 2 точки"),
NamedTextColor.DARK_GRAY)); NamedTextColor.DARK_GRAY));
meta.lore(lore); meta.lore(lore);
stack.setItemMeta(meta); stack.setItemMeta(meta);
} }
@ -204,7 +210,7 @@ public final class GroupCreateGui {
return; return;
} }
player.sendMessage(Component.text( player.sendMessage(Component.text(
"Создано шахт: " + created + (skipped > 0 ? " (пропущено существующих: " + skipped + ")" : ""), "Создано шахт: " + created + (skipped > 0 ? " (пропущено существующих: " + skipped + ")" : ""),
NamedTextColor.GREEN)); NamedTextColor.GREEN));
} }
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.gui; package dev.loki.lomines.gui;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.InventoryHolder;
@ -21,12 +21,12 @@ public final class GroupCreateGuiHolder implements InventoryHolder {
return playerId; return playerId;
} }
void setInventory(Inventory inventory) {
this.inventory = inventory;
}
@Override @Override
public Inventory getInventory() { public Inventory getInventory() {
return inventory; return inventory;
} }
void setInventory(Inventory inventory) {
this.inventory = inventory;
}
} }

View file

@ -10,4 +10,4 @@
* <li>{@code MineRewardEditor} - Editor for a specific reward</li> * <li>{@code MineRewardEditor} - Editor for a specific reward</li>
* </ul> * </ul>
*/ */
package com.loki.lomines.gui; package dev.loki.lomines.gui;

View file

@ -1,8 +1,8 @@
package com.loki.lomines.handler; package dev.loki.lomines.handler;
import com.loki.lomines.core.Mine; import dev.loki.lomines.core.Mine;
import com.loki.lomines.util.Cuboid; import dev.loki.lomines.util.format.TimeFormatter;
import com.loki.lomines.util.TimeFormatter; import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.utils.ActionBar; import dev.lolib.utils.ActionBar;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -36,9 +36,9 @@ public final class ActionBarHandler {
String message = formatMessage(); String message = formatMessage();
center.getWorld().getNearbyEntities(center, range, range, range).stream() center.getWorld().getNearbyEntities(center, range, range, range).stream()
.filter(entity -> entity instanceof Player) .filter(entity -> entity instanceof Player)
.map(entity -> (Player) entity) .map(entity -> (Player) entity)
.forEach(player -> ActionBar.send(player, message)); .forEach(player -> ActionBar.send(player, message));
} }
/** /**
@ -69,11 +69,11 @@ public final class ActionBarHandler {
String template = mine.getConfig().getActionBarMessage(); String template = mine.getConfig().getActionBarMessage();
String formatted = template String formatted = template
.replace("%mine%", mine.getName()) .replace("%mine%", mine.getName())
.replace("%percent%", String.format("%.1f", mine.getPercentFilled())) .replace("%percent%", String.format("%.1f", mine.getPercentFilled()))
.replace("%blocks%", String.valueOf(mine.getBlocks())) .replace("%blocks%", String.valueOf(mine.getBlocks()))
.replace("%total%", String.valueOf(mine.getTotalVolume())) .replace("%total%", String.valueOf(mine.getTotalVolume()))
.replace("%time%", formatTime()); .replace("%time%", formatTime());
return formatted; return formatted;
} }

View file

@ -1,8 +1,8 @@
package com.loki.lomines.handler; package dev.loki.lomines.handler;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.core.Mine; import dev.loki.lomines.core.Mine;
import com.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -26,7 +26,7 @@ public final class MineBlockHandler {
* Handles a block break event in the mine. * Handles a block break event in the mine.
* *
* @param player The player who broke the block * @param player The player who broke the block
* @param block The block that was broken * @param block The block that was broken
*/ */
public void handle(Player player, Block block) { public void handle(Player player, Block block) {
int remaining = mine.getBlocksAtomic().decrementAndGet(); int remaining = mine.getBlocksAtomic().decrementAndGet();

View file

@ -1,11 +1,10 @@
package com.loki.lomines.handler; package dev.loki.lomines.handler;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.data.config.FillMode;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.Scheduler;
import com.loki.lomines.LoMinesPlugin;
import com.loki.lomines.core.Mine;
import com.loki.lomines.data.FillMode;
import com.loki.lomines.util.Cuboid;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Location; import org.bukkit.Location;
@ -74,7 +73,7 @@ public final class MineResetHandler {
if (mine.getConfig().getFillMode() == FillMode.MASK) { if (mine.getConfig().getFillMode() == FillMode.MASK) {
mine.getBlockSetter().fillAtLocations(mine.getConfig().getMaskPositions(), placed -> mine.getBlockSetter().fillAtLocations(mine.getConfig().getMaskPositions(), placed ->
Scheduler.get(plugin).run(() -> onResetComplete(placed, silent))); Scheduler.get(plugin).run(() -> onResetComplete(placed, silent)));
return; return;
} }

View file

@ -1,8 +1,8 @@
package com.loki.lomines.handler; package dev.loki.lomines.handler;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import com.loki.lomines.data.Reward; import dev.loki.lomines.data.reward.Reward;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -30,13 +30,13 @@ public final class MineRewardHandler {
* Checks and applies rewards for a broken block. * Checks and applies rewards for a broken block.
* *
* @param player The player who broke the block * @param player The player who broke the block
* @param block The block that was broken * @param block The block that was broken
*/ */
public void checkRewards(Player player, Block block) { public void checkRewards(Player player, Block block) {
Material material = block.getType(); Material material = block.getType();
for (Reward reward : rewards) { for (Reward reward : rewards) {
if (reward.matches(material) && rollChance(reward.getChance())) { if (reward.matches(material) && rollChance(reward.chance())) {
giveReward(player, reward); giveReward(player, reward);
} }
} }
@ -59,11 +59,11 @@ public final class MineRewardHandler {
* @param reward The reward to give * @param reward The reward to give
*/ */
private void giveReward(Player player, Reward reward) { private void giveReward(Player player, Reward reward) {
for (ItemStack item : reward.getItems()) { for (ItemStack item : reward.items()) {
player.getInventory().addItem(item.clone()); player.getInventory().addItem(item.clone());
} }
for (String command : reward.getCommands()) { for (String command : reward.commands()) {
String parsed = command.replace("%player%", player.getName()); String parsed = command.replace("%player%", player.getName());
plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), parsed); plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), parsed);
} }

View file

@ -9,4 +9,4 @@
* <li>{@code ActionBarHandler} - Displays information in action bar</li> * <li>{@code ActionBarHandler} - Displays information in action bar</li>
* </ul> * </ul>
*/ */
package com.loki.lomines.handler; package dev.loki.lomines.handler;

View file

@ -0,0 +1,74 @@
package dev.loki.lomines.integration;
import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.Bukkit;
/**
* Manages integrations with external plugins.
*/
public final class IntegrationManager {
private final LoMinesPlugin plugin;
// private PlaceholderAPIIntegration placeholderAPI;
public IntegrationManager(LoMinesPlugin plugin) {
this.plugin = plugin;
}
/**
* Initializes all available integrations.
*/
public void initAll() {
// initPlaceholderAPI();
checkWorldGuard();
checkOraxen();
checkItemsAdder();
}
// private void initPlaceholderAPI() {
// if (isPluginEnabled("PlaceholderAPI")) {
// try {
// placeholderAPI = new PlaceholderAPIIntegration(plugin);
// placeholderAPI.register();
// plugin.loLogger().info("PlaceholderAPI integration enabled");
// } catch (Exception e) {
// plugin.loLogger().warn("Failed to enable PlaceholderAPI integration: " + e.getMessage());
// }
// }
// }
private void checkWorldGuard() {
if (isPluginEnabled("WorldGuard")) {
plugin.loLogger().info("WorldGuard detected (integration not yet implemented)");
}
}
private void checkOraxen() {
if (isPluginEnabled("Oraxen")) {
plugin.loLogger().info("Oraxen integration available");
}
}
private void checkItemsAdder() {
if (isPluginEnabled("ItemsAdder")) {
plugin.loLogger().info("ItemsAdder integration available");
}
}
/**
* Checks if a plugin is enabled.
*/
public boolean isPluginEnabled(String pluginName) {
return Bukkit.getPluginManager().getPlugin(pluginName) != null &&
Bukkit.getPluginManager().isPluginEnabled(pluginName);
}
/**
* Unregisters all integrations.
*/
public void shutdown() {
// if (placeholderAPI != null) {
// placeholderAPI.unregister();
// }
}
}

View file

@ -8,4 +8,4 @@
* <li>{@code WGRegionManager} - WorldGuard region management</li> * <li>{@code WGRegionManager} - WorldGuard region management</li>
* </ul> * </ul>
*/ */
package com.loki.lomines.integration; package dev.loki.lomines.integration;

View file

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

View file

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

View file

@ -1,9 +1,9 @@
package com.loki.lomines.listener; package dev.loki.lomines.listener;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.gui.GroupCreateGui; import dev.loki.lomines.gui.GroupCreateGui;
import com.loki.lomines.wand.GroupWandItem; import dev.loki.lomines.wand.GroupWandItem;
import com.loki.lomines.wand.GroupWandSession; import dev.loki.lomines.wand.GroupWandSession;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.block.Block; import org.bukkit.block.Block;
@ -53,7 +53,7 @@ public final class PlayerInteractListener implements Listener {
int slot = session.getActiveSlot(); int slot = session.getActiveSlot();
session.setPos1(slot, block.getLocation()); session.setPos1(slot, block.getLocation());
player.sendMessage(Component.text( player.sendMessage(Component.text(
"Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); "Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN));
return; return;
} }
@ -67,7 +67,7 @@ public final class PlayerInteractListener implements Listener {
int slot = session.getActiveSlot(); int slot = session.getActiveSlot();
session.setPos2(slot, block.getLocation()); session.setPos2(slot, block.getLocation());
player.sendMessage(Component.text( player.sendMessage(Component.text(
"Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); "Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN));
} }
} }
} }

View file

@ -1,6 +1,6 @@
package com.loki.lomines.listener; package dev.loki.lomines.listener;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;

View file

@ -9,4 +9,4 @@
* <li>{@code SelectionParticleTask} - Displays particles for active selections</li> * <li>{@code SelectionParticleTask} - Displays particles for active selections</li>
* </ul> * </ul>
*/ */
package com.loki.lomines.listener; package dev.loki.lomines.listener;

View file

@ -0,0 +1,84 @@
package dev.loki.lomines.util;
import org.bukkit.command.CommandSender;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Унифицированная обработка ошибок и отправка сообщений
*/
public class ErrorHandler {
private final Logger logger;
private final String prefix;
public ErrorHandler(Logger logger, String prefix) {
this.logger = logger;
this.prefix = prefix;
}
/**
* Отправить сообщение об ошибке игроку
*/
public void sendError(CommandSender sender, String message) {
sender.sendMessage(MessageFormatter.error(message));
}
/**
* Отправить сообщение об ошибке игроку с причиной
*/
public void sendError(CommandSender sender, String message, String reason) {
sender.sendMessage(MessageFormatter.error(message + ": " + reason));
}
/**
* Залогировать ошибку
*/
public void logError(String message) {
logger.log(Level.SEVERE, prefix + message);
}
/**
* Залогировать ошибку с исключением
*/
public void logError(String message, Throwable throwable) {
logger.log(Level.SEVERE, prefix + message, throwable);
}
/**
* Отправить ошибку игроку и залогировать
*/
public void handleError(CommandSender sender, String userMessage, String logMessage) {
sendError(sender, userMessage);
logError(logMessage);
}
/**
* Отправить ошибку игроку и залогировать с исключением
*/
public void handleError(CommandSender sender, String userMessage, String logMessage, Throwable throwable) {
sendError(sender, userMessage);
logError(logMessage, throwable);
}
/**
* Обработать ошибку валидации
*/
public void handleValidationError(CommandSender sender, String fieldName, String value) {
sendError(sender, "Некорректное значение для " + fieldName + ": " + value);
}
/**
* Обработать ошибку "не найдено"
*/
public void handleNotFound(CommandSender sender, String entityType, String identifier) {
sendError(sender, entityType + " '" + identifier + "' не найден");
}
/**
* Обработать ошибку доступа
*/
public void handlePermissionDenied(CommandSender sender, String action) {
sendError(sender, "У вас нет прав для: " + action);
}
}

View file

@ -0,0 +1,59 @@
package dev.loki.lomines.util;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
/**
* Utility class for formatting messages with consistent styling.
*/
public final class MessageFormatter {
private static final String PREFIX = "[LoMines] ";
private MessageFormatter() {
}
/**
* Creates a success message (green).
*/
public static Component success(String message) {
return Component.text(PREFIX + message, NamedTextColor.GREEN);
}
/**
* Creates an error message (red).
*/
public static Component error(String message) {
return Component.text(PREFIX + message, NamedTextColor.RED);
}
/**
* Creates a warning message (yellow).
*/
public static Component warning(String message) {
return Component.text(PREFIX + message, NamedTextColor.YELLOW);
}
/**
* Creates an info message (gray).
*/
public static Component info(String message) {
return Component.text(PREFIX + message, NamedTextColor.GRAY);
}
/**
* Creates a highlighted message (aqua, bold).
*/
public static Component highlight(String message) {
return Component.text(message, NamedTextColor.AQUA)
.decoration(TextDecoration.BOLD, true);
}
/**
* Creates a plain message without prefix.
*/
public static Component plain(String message) {
return Component.text(message);
}
}

View file

@ -0,0 +1,62 @@
package dev.loki.lomines.util;
import org.bukkit.Material;
import java.util.Optional;
/**
* Utility class for validation operations.
*/
public final class ValidationUtils {
private ValidationUtils() {
}
/**
* Validates and parses a material name.
*
* @param materialName the material name to validate
* @return the Material if valid
* @throws IllegalArgumentException if material is invalid
*/
public static Material validateMaterial(String materialName) {
if (materialName == null || materialName.isBlank()) {
throw new IllegalArgumentException("Material name cannot be null or empty");
}
try {
return Material.valueOf(materialName.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid material: " + materialName);
}
}
/**
* Safely parses a material name, returning Optional.
*
* @param materialName the material name to parse
* @return Optional containing the Material if valid, empty otherwise
*/
public static Optional<Material> parseMaterial(String materialName) {
if (materialName == null || materialName.isBlank()) {
return Optional.empty();
}
try {
return Optional.of(Material.valueOf(materialName.toUpperCase()));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
/**
* Parses a material name with a fallback default.
*
* @param materialName the material name to parse
* @param defaultMaterial the default material if parsing fails
* @return the parsed Material or the default
*/
public static Material parseMaterialOrDefault(String materialName, Material defaultMaterial) {
return parseMaterial(materialName).orElse(defaultMaterial);
}
}

View file

@ -1,5 +1,6 @@
package com.loki.lomines.util.format; package dev.loki.lomines.util.format;
import dev.loki.lomines.util.location.Cuboid;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.World; import org.bukkit.World;
@ -158,28 +159,16 @@ public final class ChunkUtils {
} }
/** /**
* Simple immutable holder for chunk coordinates. * Simple immutable holder for chunk coordinates.
*/ */
private static final class ChunkCoordinate { private record ChunkCoordinate(int x, int z) {
final int x;
final int z;
ChunkCoordinate(int x, int z) {
this.x = x;
this.z = z;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj) return true;
if (!(obj instanceof ChunkCoordinate)) return false; if (!(obj instanceof ChunkCoordinate other)) return false;
ChunkCoordinate other = (ChunkCoordinate) obj; return x == other.x && z == other.z;
return x == other.x && z == other.z; }
}
@Override
public int hashCode() {
return 31 * x + z;
}
} }
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.util.format; package dev.loki.lomines.util.format;
/** /**
* Utility class for formatting time from Minecraft ticks to human-readable formats. * Utility class for formatting time from Minecraft ticks to human-readable formats.
@ -17,7 +17,7 @@ public final class TimeFormatter {
/** /**
* Formats time from Minecraft ticks to a human-readable string. * Formats time from Minecraft ticks to a human-readable string.
* *
* @param ticks the number of Minecraft ticks * @param ticks the number of Minecraft ticks
* @param format the desired format: "mm:ss", "hh:mm:ss", "s", "m", or "h" * @param format the desired format: "mm:ss", "hh:mm:ss", "s", "m", or "h"
* @return the formatted time string * @return the formatted time string
* @throws IllegalArgumentException if the format is not recognized * @throws IllegalArgumentException if the format is not recognized
@ -40,8 +40,8 @@ public final class TimeFormatter {
case "m" -> String.valueOf(totalSeconds / SECONDS_PER_MINUTE); case "m" -> String.valueOf(totalSeconds / SECONDS_PER_MINUTE);
case "h" -> String.valueOf(totalSeconds / (SECONDS_PER_MINUTE * MINUTES_PER_HOUR)); case "h" -> String.valueOf(totalSeconds / (SECONDS_PER_MINUTE * MINUTES_PER_HOUR));
default -> throw new IllegalArgumentException( default -> throw new IllegalArgumentException(
"Unknown time format: '" + format + "'. " + "Unknown time format: '" + format + "'. " +
"Supported formats: mm:ss, hh:mm:ss, s, m, h" "Supported formats: mm:ss, hh:mm:ss, s, m, h"
); );
}; };
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.util.location; package dev.loki.lomines.util.location;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.block.Block; import org.bukkit.block.Block;

View file

@ -1,4 +1,4 @@
package com.loki.lomines.util.location; package dev.loki.lomines.util.location;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
@ -72,8 +72,8 @@ public final class Cuboid {
int z = location.getBlockZ(); int z = location.getBlockZ();
return x >= minX && x <= maxX return x >= minX && x <= maxX
&& y >= minY && y <= maxY && y >= minY && y <= maxY
&& z >= minZ && z <= maxZ; && z >= minZ && z <= maxZ;
} }
/** /**
@ -154,18 +154,17 @@ public final class Cuboid {
@Override @Override
public String toString() { public String toString() {
return String.format("Cuboid[world=%s, min=(%d,%d,%d), max=(%d,%d,%d), volume=%d]", return String.format("Cuboid[world=%s, min=(%d,%d,%d), max=(%d,%d,%d), volume=%d]",
world.getName(), minX, minY, minZ, maxX, maxY, maxZ, getVolume()); world.getName(), minX, minY, minZ, maxX, maxY, maxZ, getVolume());
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj) return true;
if (!(obj instanceof Cuboid)) return false; if (!(obj instanceof Cuboid other)) return false;
Cuboid other = (Cuboid) obj;
return world.equals(other.world) return world.equals(other.world)
&& minX == other.minX && minY == other.minY && minZ == other.minZ && minX == other.minX && minY == other.minY && minZ == other.minZ
&& maxX == other.maxX && maxY == other.maxY && maxZ == other.maxZ; && maxX == other.maxX && maxY == other.maxY && maxZ == other.maxZ;
} }
@Override @Override

View file

@ -1,6 +1,6 @@
package com.loki.lomines.util.location; package dev.loki.lomines.util.location;
import com.loki.lomines.data.config.ConfigParseException; import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
@ -16,7 +16,7 @@ import java.util.regex.Pattern;
public final class LocationParser { public final class LocationParser {
private static final Pattern LOCATION_PATTERN = private static final Pattern LOCATION_PATTERN =
Pattern.compile("([^;]+);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*)"); Pattern.compile("([^;]+);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*)");
private LocationParser() { private LocationParser() {
throw new UnsupportedOperationException("Utility class"); throw new UnsupportedOperationException("Utility class");
@ -37,8 +37,8 @@ public final class LocationParser {
Matcher matcher = LOCATION_PATTERN.matcher(locationString.trim()); Matcher matcher = LOCATION_PATTERN.matcher(locationString.trim());
if (!matcher.matches()) { if (!matcher.matches()) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid location format: '" + locationString + "'. " + "Invalid location format: '" + locationString + "'. " +
"Expected format: 'world;x;y;z;yaw;pitch'" "Expected format: 'world;x;y;z;yaw;pitch'"
); );
} }
@ -53,7 +53,7 @@ public final class LocationParser {
World world = Bukkit.getWorld(worldName); World world = Bukkit.getWorld(worldName);
if (world == null) { if (world == null) {
throw new ConfigParseException( throw new ConfigParseException(
"World '" + worldName + "' does not exist or is not loaded" "World '" + worldName + "' does not exist or is not loaded"
); );
} }
@ -61,8 +61,8 @@ public final class LocationParser {
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new ConfigParseException( throw new ConfigParseException(
"Invalid number format in location string: '" + locationString + "'", "Invalid number format in location string: '" + locationString + "'",
e e
); );
} }
} }
@ -85,12 +85,12 @@ public final class LocationParser {
} }
return String.format(Locale.US, "%s;%.2f;%.2f;%.2f;%.2f;%.2f", return String.format(Locale.US, "%s;%.2f;%.2f;%.2f;%.2f;%.2f",
world.getName(), world.getName(),
location.getX(), location.getX(),
location.getY(), location.getY(),
location.getZ(), location.getZ(),
location.getYaw(), location.getYaw(),
location.getPitch() location.getPitch()
); );
} }
} }

View file

@ -11,4 +11,4 @@
* <li>{@code ChunkUtils} - Utilities for chunk operations</li> * <li>{@code ChunkUtils} - Utilities for chunk operations</li>
* </ul> * </ul>
*/ */
package com.loki.lomines.util; package dev.loki.lomines.util;

View file

@ -1,5 +1,6 @@
package com.loki.lomines.util.selection; package dev.loki.lomines.util.selection;
import dev.loki.lomines.util.location.Cuboid;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;

View file

@ -1,5 +1,6 @@
package com.loki.lomines.util.selection; package dev.loki.lomines.util.selection;
import dev.loki.lomines.util.location.Cuboid;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.ArrayList; import java.util.ArrayList;
@ -30,7 +31,7 @@ public final class Selection {
/** /**
* Sets a point at the specified index. * Sets a point at the specified index.
* *
* @param index The point index (0-9) * @param index The point index (0-9)
* @param location The location to set * @param location The location to set
* @throws IllegalArgumentException if index is out of bounds * @throws IllegalArgumentException if index is out of bounds
*/ */
@ -63,7 +64,7 @@ public final class Selection {
validatePairIndex(pairIndex); validatePairIndex(pairIndex);
int firstIndex = pairIndex * 2; int firstIndex = pairIndex * 2;
int secondIndex = firstIndex + 1; int secondIndex = firstIndex + 1;
return new Location[] { points[firstIndex], points[secondIndex] }; return new Location[]{points[firstIndex], points[secondIndex]};
} }
/** /**
@ -117,7 +118,7 @@ public final class Selection {
private void validateIndex(int index) { private void validateIndex(int index) {
if (index < 0 || index >= MAX_POINTS) { if (index < 0 || index >= MAX_POINTS) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Point index must be between 0 and " + (MAX_POINTS - 1) + ", got: " + index "Point index must be between 0 and " + (MAX_POINTS - 1) + ", got: " + index
); );
} }
} }
@ -131,7 +132,7 @@ public final class Selection {
private void validatePairIndex(int pairIndex) { private void validatePairIndex(int pairIndex) {
if (pairIndex < 0 || pairIndex >= MAX_PAIRS) { if (pairIndex < 0 || pairIndex >= MAX_PAIRS) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Pair index must be between 0 and " + (MAX_PAIRS - 1) + ", got: " + pairIndex "Pair index must be between 0 and " + (MAX_PAIRS - 1) + ", got: " + pairIndex
); );
} }
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.util.selection; package dev.loki.lomines.util.selection;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;

View file

@ -1,6 +1,6 @@
package com.loki.lomines.wand; package dev.loki.lomines.wand;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material; import org.bukkit.Material;
@ -17,7 +17,9 @@ import java.util.List;
public final class GroupWandItem { public final class GroupWandItem {
private static final String KEY = "group_wand"; private static final String KEY = "group_wand";
/** Same namespace as {@code plugin.yml} name, avoids IDE needing full {@link org.bukkit.plugin.Plugin} classpath. */ /**
* Same namespace as {@code plugin.yml} name, avoids IDE needing full {@link org.bukkit.plugin.Plugin} classpath.
*/
private static final String NAMESPACE = "lomines"; private static final String NAMESPACE = "lomines";
private GroupWandItem() { private GroupWandItem() {
@ -33,12 +35,12 @@ public final class GroupWandItem {
if (meta != null) { if (meta != null) {
meta.displayName(Component.text("LoMines · группа", NamedTextColor.GOLD)); meta.displayName(Component.text("LoMines · группа", NamedTextColor.GOLD));
meta.lore(List.of( meta.lore(List.of(
Component.text("ЛКМ блок — угол 1 (слот ", NamedTextColor.GRAY) Component.text("ЛКМ блок — угол 1 (слот ", NamedTextColor.GRAY)
.append(Component.text("1–9", NamedTextColor.YELLOW)) .append(Component.text("1–9", NamedTextColor.YELLOW))
.append(Component.text(" в меню)", NamedTextColor.GRAY)), .append(Component.text(" в меню)", NamedTextColor.GRAY)),
Component.text("ПКМ блок — угол 2", NamedTextColor.GRAY), Component.text("ПКМ блок — угол 2", NamedTextColor.GRAY),
Component.text("Shift+ПКМ — меню: 9 шахт, одни настройки", NamedTextColor.GRAY), Component.text("Shift+ПКМ — меню: 9 шахт, одни настройки", NamedTextColor.GRAY),
Component.text("/lm group <префикс> — имена префикс_1…префикс_9", NamedTextColor.DARK_GRAY) Component.text("/lm group <префикс> — имена префикс_1…префикс_9", NamedTextColor.DARK_GRAY)
)); ));
meta.getPersistentDataContainer().set(key(plugin), PersistentDataType.BYTE, (byte) 1); meta.getPersistentDataContainer().set(key(plugin), PersistentDataType.BYTE, (byte) 1);
item.setItemMeta(meta); item.setItemMeta(meta);

View file

@ -1,4 +1,4 @@
package com.loki.lomines.wand; package dev.loki.lomines.wand;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;

View file

@ -1,4 +1,4 @@
package com.loki.lomines.wand; package dev.loki.lomines.wand;
import org.bukkit.Location; import org.bukkit.Location;
@ -11,11 +11,10 @@ import java.util.Arrays;
public final class GroupWandSession { public final class GroupWandSession {
private static final int SLOTS = 9; private static final int SLOTS = 9;
private String baseName = "";
private int activeSlot;
private final Location[] pos1 = new Location[SLOTS]; private final Location[] pos1 = new Location[SLOTS];
private final Location[] pos2 = new Location[SLOTS]; private final Location[] pos2 = new Location[SLOTS];
private String baseName = "";
private int activeSlot;
public String getBaseName() { public String getBaseName() {
return baseName; return baseName;

View file

@ -1,11 +1,11 @@
name: LoMines name: LoMines
version: ${version} version: ${version}
description: ${description} description: ${description}
main: com.loki.lomines.LoMinesPlugin main: dev.loki.lomines.LoMinesPlugin
author: loki author: loki
api-version: "1.21" api-version: "1.21"
website: https://github.com/loki/lomines website: https://github.com/loki/lomines
softdepend: [PlaceholderAPI, WorldGuard, Oraxen, ItemsAdder] softdepend: [ PlaceholderAPI, WorldGuard, Oraxen, ItemsAdder ]
# LoAPI will handle dependency loading via DependencyManager # LoAPI will handle dependency loading via DependencyManager
# Dependencies: commons-math3 and commons-io will be loaded at runtime # Dependencies: commons-math3 and commons-io will be loaded at runtime
@ -13,7 +13,7 @@ softdepend: [PlaceholderAPI, WorldGuard, Oraxen, ItemsAdder]
commands: commands:
lm: lm:
description: Main LoMines command description: Main LoMines command
aliases: [lomines, mine, mines] aliases: [ lomines, mine, mines ]
permission: lomines.use permission: lomines.use
usage: /<command> <subcommand> usage: /<command> <subcommand>

View file

@ -1,8 +1,8 @@
package com.loki.lomines.command; package dev.loki.lomines.command;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.core.Mine; import dev.loki.lomines.core.Mine;
import com.loki.lomines.core.Mines; import dev.loki.lomines.core.Mines;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -13,6 +13,7 @@ import org.mockito.MockitoAnnotations;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -128,7 +129,7 @@ class AdminCommandsTest {
@Test @Test
void testListMinesEmpty() { void testListMinesEmpty() {
when(mines.getAll()).thenReturn(Arrays.asList()); when(mines.getAll()).thenReturn(List.of());
commands.list(sender); commands.list(sender);

View file

@ -1,10 +1,10 @@
package com.loki.lomines.core; package dev.loki.lomines.core;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import com.loki.lomines.block.BukkitBlockSetter; import dev.loki.lomines.block.BukkitBlockSetter;
import com.loki.lomines.block.ItemsAdderBlockSetter; import dev.loki.lomines.block.ItemsAdderBlockSetter;
import com.loki.lomines.block.OraxenBlockSetter; import dev.loki.lomines.block.OraxenBlockSetter;
import com.loki.lomines.data.MineConfig; import dev.loki.lomines.data.MineConfig;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -49,8 +49,7 @@ class MineTest {
// Assert // Assert
assertNotNull(mine.getBlockSetter()); assertNotNull(mine.getBlockSetter());
assertTrue(mine.getBlockSetter() instanceof OraxenBlockSetter, assertInstanceOf(OraxenBlockSetter.class, mine.getBlockSetter(), "Expected OraxenBlockSetter for oraxen: prefix");
"Expected OraxenBlockSetter for oraxen: prefix");
} }
@Test @Test
@ -67,8 +66,7 @@ class MineTest {
// Assert // Assert
assertNotNull(mine.getBlockSetter()); assertNotNull(mine.getBlockSetter());
assertTrue(mine.getBlockSetter() instanceof ItemsAdderBlockSetter, assertInstanceOf(ItemsAdderBlockSetter.class, mine.getBlockSetter(), "Expected ItemsAdderBlockSetter for itemsadder: prefix");
"Expected ItemsAdderBlockSetter for itemsadder: prefix");
} }
@Test @Test
@ -86,8 +84,7 @@ class MineTest {
// Assert // Assert
assertNotNull(mine.getBlockSetter()); assertNotNull(mine.getBlockSetter());
assertTrue(mine.getBlockSetter() instanceof BukkitBlockSetter, assertInstanceOf(BukkitBlockSetter.class, mine.getBlockSetter(), "Expected BukkitBlockSetter for vanilla blocks without prefix");
"Expected BukkitBlockSetter for vanilla blocks without prefix");
} }
@Test @Test

View file

@ -1,12 +1,12 @@
package com.loki.lomines.core; package dev.loki.lomines.core;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/** /**
* Unit tests for MineTicker class. * Unit tests for MineTicker class.

View file

@ -1,9 +1,9 @@
package com.loki.lomines.core; package dev.loki.lomines.core;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger; import dev.lolib.core.LoLogger;
import dev.lolib.scheduler.Scheduler;
import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.Server; import org.bukkit.Server;
@ -29,13 +29,12 @@ import static org.mockito.Mockito.*;
*/ */
class MinesTest { class MinesTest {
@TempDir
Path tempDir;
private Mines mines; private Mines mines;
private LoMinesPlugin plugin; private LoMinesPlugin plugin;
private Scheduler scheduler; private Scheduler scheduler;
@TempDir
Path tempDir;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
plugin = mock(LoMinesPlugin.class); plugin = mock(LoMinesPlugin.class);
@ -101,24 +100,24 @@ class MinesTest {
// Create a valid mine config file // Create a valid mine config file
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: 100 stone: 100
reset: reset:
ticks: 6000 ticks: 6000
percent: 10.0 percent: 10.0
reset-on-percent: false reset-on-percent: false
actionbar: actionbar:
enabled: true enabled: true
message: "Test mine" message: "Test mine"
range: 50.0 range: 50.0
timer-format: "mm:ss" timer-format: "mm:ss"
teleport-on-reset: false teleport-on-reset: false
reset-commands: [] reset-commands: []
broadcast-reset: "" broadcast-reset: ""
random-rewards: [] random-rewards: []
"""; """;
Path configFile = minesFolder.resolve("testmine.yml"); Path configFile = minesFolder.resolve("testmine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -150,24 +149,24 @@ class MinesTest {
// Create multiple mine config files // Create multiple mine config files
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: 100 stone: 100
reset: reset:
ticks: 6000 ticks: 6000
percent: 10.0 percent: 10.0
reset-on-percent: false reset-on-percent: false
actionbar: actionbar:
enabled: true enabled: true
message: "Test mine" message: "Test mine"
range: 50.0 range: 50.0
timer-format: "mm:ss" timer-format: "mm:ss"
teleport-on-reset: false teleport-on-reset: false
reset-commands: [] reset-commands: []
broadcast-reset: "" broadcast-reset: ""
random-rewards: [] random-rewards: []
"""; """;
Files.writeString(minesFolder.resolve("mine1.yml"), yamlContent); Files.writeString(minesFolder.resolve("mine1.yml"), yamlContent);
Files.writeString(minesFolder.resolve("mine2.yml"), yamlContent); Files.writeString(minesFolder.resolve("mine2.yml"), yamlContent);
@ -198,14 +197,14 @@ class MinesTest {
// Create a valid mine config file // Create a valid mine config file
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: 100 stone: 100
reset: reset:
ticks: 6000 ticks: 6000
percent: 10.0 percent: 10.0
"""; """;
Files.writeString(minesFolder.resolve("validmine.yml"), yamlContent); Files.writeString(minesFolder.resolve("validmine.yml"), yamlContent);
Files.writeString(minesFolder.resolve("readme.txt"), "This is not a mine config"); Files.writeString(minesFolder.resolve("readme.txt"), "This is not a mine config");
@ -234,8 +233,8 @@ class MinesTest {
// Create an invalid mine config file (missing required fields) // Create an invalid mine config file (missing required fields)
String invalidYaml = """ String invalidYaml = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
"""; """;
Files.writeString(minesFolder.resolve("invalidmine.yml"), invalidYaml); Files.writeString(minesFolder.resolve("invalidmine.yml"), invalidYaml);

View file

@ -1,10 +1,10 @@
package com.loki.lomines.data; package dev.loki.lomines.data;
import org.bukkit.Bukkit; import dev.loki.lomines.data.config.ConfigLoader;
import org.bukkit.Location; import dev.loki.lomines.data.config.MineConfig;
import org.bukkit.Material; import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.Server; import dev.loki.lomines.data.reward.Reward;
import org.bukkit.World; import org.bukkit.*;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@ -26,10 +26,9 @@ import static org.mockito.Mockito.*;
*/ */
class ConfigLoaderTest { class ConfigLoaderTest {
private ConfigLoader configLoader;
@TempDir @TempDir
Path tempDir; Path tempDir;
private ConfigLoader configLoader;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@ -40,36 +39,36 @@ class ConfigLoaderTest {
void testLoadValidConfig() throws Exception { void testLoadValidConfig() throws Exception {
// Create a valid YAML config file // Create a valid YAML config file
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: 50 stone: 50
coal_ore: 30 coal_ore: 30
iron_ore: 20 iron_ore: 20
reset: reset:
ticks: 6000 ticks: 6000
percent: 10.0 percent: 10.0
reset-on-percent: false reset-on-percent: false
actionbar: actionbar:
enabled: true enabled: true
message: "Mine resetting soon" message: "Mine resetting soon"
range: 50.0 range: 50.0
timer-format: "mm:ss" timer-format: "mm:ss"
teleport-on-reset: false teleport-on-reset: false
reset-commands: reset-commands:
- "say Mine reset!" - "say Mine reset!"
broadcast-reset: "Mine has been reset" broadcast-reset: "Mine has been reset"
random-rewards: random-rewards:
- chance: 1.0 - chance: 1.0
prevent-drops: false prevent-drops: false
blocks: blocks:
- diamond_ore - diamond_ore
items: items:
- type: diamond - type: diamond
amount: 1 amount: 1
commands: commands:
- "eco give %player% 100" - "eco give %player% 100"
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -151,9 +150,9 @@ class ConfigLoaderTest {
@Test @Test
void testLoadMissingSelections() throws Exception { void testLoadMissingSelections() throws Exception {
String yamlContent = """ String yamlContent = """
contents: contents:
stone: 100 stone: 100
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -168,12 +167,12 @@ class ConfigLoaderTest {
@Test @Test
void testLoadOddNumberOfSelections() throws Exception { void testLoadOddNumberOfSelections() throws Exception {
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
selection.3: world;20;64;20;0.0;0.0 selection.3: world;20;64;20;0.0;0.0
contents: contents:
stone: 100 stone: 100
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -198,9 +197,9 @@ class ConfigLoaderTest {
@Test @Test
void testLoadMissingContents() throws Exception { void testLoadMissingContents() throws Exception {
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -225,11 +224,11 @@ class ConfigLoaderTest {
@Test @Test
void testLoadInvalidMaterial() throws Exception { void testLoadInvalidMaterial() throws Exception {
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
invalid_material: 100 invalid_material: 100
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -254,11 +253,11 @@ class ConfigLoaderTest {
@Test @Test
void testLoadInvalidBlockWeight() throws Exception { void testLoadInvalidBlockWeight() throws Exception {
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: -10 stone: -10
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -283,15 +282,15 @@ class ConfigLoaderTest {
@Test @Test
void testLoadInvalidRewardChance() throws Exception { void testLoadInvalidRewardChance() throws Exception {
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: 100 stone: 100
random-rewards: random-rewards:
- chance: 150 - chance: 150
blocks: blocks:
- diamond_ore - diamond_ore
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -316,13 +315,13 @@ class ConfigLoaderTest {
@Test @Test
void testLoadWithTeleportLocation() throws Exception { void testLoadWithTeleportLocation() throws Exception {
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: 100 stone: 100
teleport-on-reset: true teleport-on-reset: true
teleport-location: world;5;70;5;90.0;0.0 teleport-location: world;5;70;5;90.0;0.0
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -351,32 +350,32 @@ class ConfigLoaderTest {
void testSaveAndLoadRoundTrip() throws Exception { void testSaveAndLoadRoundTrip() throws Exception {
// Create a config file to load (without items to avoid ItemStack initialization issues) // Create a config file to load (without items to avoid ItemStack initialization issues)
String yamlContent = """ String yamlContent = """
selection.1: world;0;64;0;0.0;0.0 selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0 selection.2: world;10;74;10;0.0;0.0
contents: contents:
stone: 50 stone: 50
coal_ore: 30 coal_ore: 30
reset: reset:
ticks: 6000 ticks: 6000
percent: 10.0 percent: 10.0
reset-on-percent: false reset-on-percent: false
actionbar: actionbar:
enabled: true enabled: true
message: "Mine resetting soon" message: "Mine resetting soon"
range: 50.0 range: 50.0
timer-format: "mm:ss" timer-format: "mm:ss"
teleport-on-reset: false teleport-on-reset: false
reset-commands: reset-commands:
- "say Mine reset!" - "say Mine reset!"
broadcast-reset: "Mine has been reset" broadcast-reset: "Mine has been reset"
random-rewards: random-rewards:
- chance: 1.0 - chance: 1.0
prevent-drops: false prevent-drops: false
blocks: blocks:
- diamond_ore - diamond_ore
commands: commands:
- "eco give %player% 100" - "eco give %player% 100"
"""; """;
Path configFile = tempDir.resolve("test-mine.yml"); Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent); Files.writeString(configFile, yamlContent);
@ -450,10 +449,10 @@ class ConfigLoaderTest {
Location loc2 = new Location(world, 10, 74, 10, 0, 0); Location loc2 = new Location(world, 10, 74, 10, 0, 0);
MineConfig config = MineConfig.builder() MineConfig config = MineConfig.builder()
.selections(List.of(loc1, loc2)) .selections(List.of(loc1, loc2))
.blocks(Map.of("stone", 100.0)) .blocks(Map.of("stone", 100.0))
.rewards(List.of()) .rewards(List.of())
.build(); .build();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
configLoader.save(config, null); configLoader.save(config, null);
@ -479,12 +478,12 @@ class ConfigLoaderTest {
Location teleportLoc = new Location(world, 5, 70, 5, 90, 0); Location teleportLoc = new Location(world, 5, 70, 5, 90, 0);
MineConfig config = MineConfig.builder() MineConfig config = MineConfig.builder()
.selections(List.of(loc1, loc2)) .selections(List.of(loc1, loc2))
.blocks(Map.of("stone", 100.0)) .blocks(Map.of("stone", 100.0))
.rewards(List.of()) .rewards(List.of())
.teleportOnReset(true) .teleportOnReset(true)
.teleportLocation(teleportLoc) .teleportLocation(teleportLoc)
.build(); .build();
Path savedFile = tempDir.resolve("with-teleport.yml"); Path savedFile = tempDir.resolve("with-teleport.yml");
configLoader.save(config, savedFile); configLoader.save(config, savedFile);
@ -517,10 +516,10 @@ class ConfigLoaderTest {
Location loc2 = new Location(world, 10, 74, 10, 0, 0); Location loc2 = new Location(world, 10, 74, 10, 0, 0);
MineConfig config = MineConfig.builder() MineConfig config = MineConfig.builder()
.selections(List.of(loc1, loc2)) .selections(List.of(loc1, loc2))
.blocks(Map.of("stone", 100.0)) .blocks(Map.of("stone", 100.0))
.rewards(List.of()) .rewards(List.of())
.build(); .build();
Path savedFile = tempDir.resolve("no-rewards.yml"); Path savedFile = tempDir.resolve("no-rewards.yml");
configLoader.save(config, savedFile); configLoader.save(config, savedFile);

View file

@ -1,6 +1,6 @@
package com.loki.lomines.data; package dev.loki.lomines.data;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger; import dev.lolib.core.LoLogger;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -9,8 +9,10 @@ import java.io.File;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/** /**
* Integration tests for Leaderboard caching with StatsManager. * Integration tests for Leaderboard caching with StatsManager.
@ -128,7 +130,7 @@ class LeaderboardCacheIntegrationTest {
// Cache hit should be significantly faster (at least 2x) // Cache hit should be significantly faster (at least 2x)
assertTrue(time2 < time1, assertTrue(time2 < time1,
String.format("Cache hit (%d ns) should be faster than cache miss (%d ns)", time2, time1)); String.format("Cache hit (%d ns) should be faster than cache miss (%d ns)", time2, time1));
} }
@Test @Test

View file

@ -1,6 +1,6 @@
package com.loki.lomines.data; package dev.loki.lomines.data;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger; import dev.lolib.core.LoLogger;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -9,8 +9,10 @@ import java.io.File;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/** /**
* Unit tests for Leaderboard class. * Unit tests for Leaderboard class.

View file

@ -1,9 +1,9 @@
package com.loki.lomines.data; package dev.loki.lomines.data;
import com.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger; import dev.lolib.core.LoLogger;
import dev.lolib.scheduler.Scheduler;
import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -26,13 +26,12 @@ import static org.mockito.Mockito.*;
*/ */
class StatsManagerTest { class StatsManagerTest {
@TempDir
Path tempDir;
private StatsManager statsManager; private StatsManager statsManager;
private LoMinesPlugin plugin; private LoMinesPlugin plugin;
private Scheduler scheduler; private Scheduler scheduler;
@TempDir
Path tempDir;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
plugin = mock(LoMinesPlugin.class); plugin = mock(LoMinesPlugin.class);
@ -116,7 +115,7 @@ class StatsManagerTest {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(statsFile.toFile()); YamlConfiguration yaml = YamlConfiguration.loadConfiguration(statsFile.toFile());
assertNotNull(yaml.getConfigurationSection("players")); assertNotNull(yaml.getConfigurationSection("players"));
String playerPath = "players." + playerId.toString(); String playerPath = "players." + playerId;
assertEquals(3, yaml.getLong(playerPath + ".total")); assertEquals(3, yaml.getLong(playerPath + ".total"));
assertEquals(2, yaml.getLong(playerPath + ".mines.testmine")); assertEquals(2, yaml.getLong(playerPath + ".mines.testmine"));
assertEquals(1, yaml.getLong(playerPath + ".mines.anothermine")); assertEquals(1, yaml.getLong(playerPath + ".mines.anothermine"));
@ -145,13 +144,13 @@ class StatsManagerTest {
void testLoadValidStats() throws IOException { void testLoadValidStats() throws IOException {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
String yamlContent = String.format(""" String yamlContent = String.format("""
players: players:
%s: %s:
total: 100 total: 100
mines: mines:
mine1: 60 mine1: 60
mine2: 40 mine2: 40
""", playerId.toString()); """, playerId);
Path statsFile = tempDir.resolve("stats.yml"); Path statsFile = tempDir.resolve("stats.yml");
Files.writeString(statsFile, yamlContent); Files.writeString(statsFile, yamlContent);
@ -167,10 +166,10 @@ class StatsManagerTest {
@Test @Test
void testLoadInvalidUUID() throws IOException { void testLoadInvalidUUID() throws IOException {
String yamlContent = """ String yamlContent = """
players: players:
invalid-uuid: invalid-uuid:
total: 100 total: 100
"""; """;
Path statsFile = tempDir.resolve("stats.yml"); Path statsFile = tempDir.resolve("stats.yml");
Files.writeString(statsFile, yamlContent); Files.writeString(statsFile, yamlContent);

View file

@ -1,4 +1,4 @@
package com.loki.lomines.util; package dev.loki.lomines.util;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Location; import org.bukkit.Location;
@ -11,7 +11,8 @@ import org.mockito.MockitoAnnotations;
import java.util.Collections; import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;

View file

@ -1,6 +1,6 @@
package com.loki.lomines.util; package dev.loki.lomines.util;
import com.loki.lomines.data.ConfigParseException; import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
@ -9,7 +9,8 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class LocationParserTest { class LocationParserTest {
@ -68,8 +69,8 @@ class LocationParserTest {
@Test @Test
void testParseNullString() { void testParseNullString() {
ConfigParseException exception = assertThrows( ConfigParseException exception = assertThrows(
ConfigParseException.class, ConfigParseException.class,
() -> LocationParser.parse(null) () -> LocationParser.parse(null)
); );
assertTrue(exception.getMessage().contains("cannot be null or empty")); assertTrue(exception.getMessage().contains("cannot be null or empty"));
} }
@ -77,8 +78,8 @@ class LocationParserTest {
@Test @Test
void testParseEmptyString() { void testParseEmptyString() {
ConfigParseException exception = assertThrows( ConfigParseException exception = assertThrows(
ConfigParseException.class, ConfigParseException.class,
() -> LocationParser.parse("") () -> LocationParser.parse("")
); );
assertTrue(exception.getMessage().contains("cannot be null or empty")); assertTrue(exception.getMessage().contains("cannot be null or empty"));
} }
@ -86,8 +87,8 @@ class LocationParserTest {
@Test @Test
void testParseInvalidFormat() { void testParseInvalidFormat() {
ConfigParseException exception = assertThrows( ConfigParseException exception = assertThrows(
ConfigParseException.class, ConfigParseException.class,
() -> LocationParser.parse("world;100;64") () -> LocationParser.parse("world;100;64")
); );
assertTrue(exception.getMessage().contains("Invalid location format")); assertTrue(exception.getMessage().contains("Invalid location format"));
assertTrue(exception.getMessage().contains("Expected format")); assertTrue(exception.getMessage().contains("Expected format"));
@ -96,8 +97,8 @@ class LocationParserTest {
@Test @Test
void testParseInvalidNumbers() { void testParseInvalidNumbers() {
ConfigParseException exception = assertThrows( ConfigParseException exception = assertThrows(
ConfigParseException.class, ConfigParseException.class,
() -> LocationParser.parse("world;abc;64;0;0;0") () -> LocationParser.parse("world;abc;64;0;0;0")
); );
assertTrue(exception.getMessage().contains("Invalid location format")); assertTrue(exception.getMessage().contains("Invalid location format"));
} }
@ -108,8 +109,8 @@ class LocationParserTest {
bukkit.when(() -> Bukkit.getWorld("nonexistent")).thenReturn(null); bukkit.when(() -> Bukkit.getWorld("nonexistent")).thenReturn(null);
ConfigParseException exception = assertThrows( ConfigParseException exception = assertThrows(
ConfigParseException.class, ConfigParseException.class,
() -> LocationParser.parse("nonexistent;0;64;0;0;0") () -> LocationParser.parse("nonexistent;0;64;0;0;0")
); );
assertTrue(exception.getMessage().contains("does not exist")); assertTrue(exception.getMessage().contains("does not exist"));
assertTrue(exception.getMessage().contains("nonexistent")); assertTrue(exception.getMessage().contains("nonexistent"));
@ -141,8 +142,8 @@ class LocationParserTest {
@Test @Test
void testFormatNullLocation() { void testFormatNullLocation() {
IllegalArgumentException exception = assertThrows( IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> LocationParser.format(null) () -> LocationParser.format(null)
); );
assertTrue(exception.getMessage().contains("Location cannot be null")); assertTrue(exception.getMessage().contains("Location cannot be null"));
} }
@ -152,8 +153,8 @@ class LocationParserTest {
Location location = new Location(null, 0, 0, 0); Location location = new Location(null, 0, 0, 0);
IllegalArgumentException exception = assertThrows( IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> LocationParser.format(location) () -> LocationParser.format(location)
); );
assertTrue(exception.getMessage().contains("world cannot be null")); assertTrue(exception.getMessage().contains("world cannot be null"));
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.util; package dev.loki.lomines.util;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;

View file

@ -1,4 +1,4 @@
package com.loki.lomines.util; package dev.loki.lomines.util;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;

View file

@ -1,8 +1,9 @@
package com.loki.lomines.util; package dev.loki.lomines.util;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TimeFormatterTest { class TimeFormatterTest {
@ -113,8 +114,8 @@ class TimeFormatterTest {
@Test @Test
void testFormatNullFormat() { void testFormatNullFormat() {
IllegalArgumentException exception = assertThrows( IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> TimeFormatter.format(1200, null) () -> TimeFormatter.format(1200, null)
); );
assertTrue(exception.getMessage().contains("cannot be null or empty")); assertTrue(exception.getMessage().contains("cannot be null or empty"));
} }
@ -122,8 +123,8 @@ class TimeFormatterTest {
@Test @Test
void testFormatEmptyFormat() { void testFormatEmptyFormat() {
IllegalArgumentException exception = assertThrows( IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> TimeFormatter.format(1200, "") () -> TimeFormatter.format(1200, "")
); );
assertTrue(exception.getMessage().contains("cannot be null or empty")); assertTrue(exception.getMessage().contains("cannot be null or empty"));
} }
@ -131,8 +132,8 @@ class TimeFormatterTest {
@Test @Test
void testFormatWhitespaceFormat() { void testFormatWhitespaceFormat() {
IllegalArgumentException exception = assertThrows( IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> TimeFormatter.format(1200, " ") () -> TimeFormatter.format(1200, " ")
); );
assertTrue(exception.getMessage().contains("cannot be null or empty")); assertTrue(exception.getMessage().contains("cannot be null or empty"));
} }
@ -140,8 +141,8 @@ class TimeFormatterTest {
@Test @Test
void testFormatUnknownFormat() { void testFormatUnknownFormat() {
IllegalArgumentException exception = assertThrows( IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> TimeFormatter.format(1200, "invalid") () -> TimeFormatter.format(1200, "invalid")
); );
assertTrue(exception.getMessage().contains("Unknown time format")); assertTrue(exception.getMessage().contains("Unknown time format"));
assertTrue(exception.getMessage().contains("invalid")); assertTrue(exception.getMessage().contains("invalid"));

259
todo.md Normal file
View file

@ -0,0 +1,259 @@
# LoMines - TODO List (Обновлено: 2026-05-31)
> **Статус проекта:** Рефакторинг завершен, пакеты изменены com.loki -> dev.loki
> **Базовая библиотека:** LoAPI (jar из `libs/lolib*.jar`, сейчас 3.0.0)
> **Версия Minecraft:** Paper 1.21.4
> **Java:** 21
> **Цель:** Привести код к стандартам качества + убрать зависимость от AXAPI
---
## 📋 Общий прогресс
- [x] **Phase 1:** Структура проекта и конфигурация (5/5) ✅
- [x] **Phase 2:** Core классы и handlers (тикер, reload) ✅
- [x] **Phase 3:** BlockSetter — тип по `Map<String, Double>` и prefix ✅
- [x] **Phase 4:** Команды — разбито на 4 класса ✅
- [x] **Phase 5:** Рефакторинг больших файлов (3/3 критичных) ✅
- [x] **Phase 6:** Организация папок (все ≤6 файлов) ✅
- [x] **Phase 7:** Утилиты и хелперы (3/3 созданы) ✅
- [x] **Phase 8:** Рефакторинг пакетов com.loki -> dev.loki ✅
- [ ] **Phase 9:** GUI редакторы (0/5, кроме `GroupCreateGui`)
- [ ] **Phase 10:** Интеграции — `IntegrationManager` только детектит плагины
- [ ] **Phase 11:** Тестирование — JUnit в `src/test` (импорты исправлены)
**Рабочий код:** ~95%; **Качество кода:** отлично
**Статус компиляции:** Готов к компиляции
- Gradle 8.14 (wrapper требуется восстановить)
- Java 21 ✅
- Paper API 1.21.4 ✅
- Временно отключены: PlaceholderAPI, Oraxen, ItemsAdder интеграции
---
## 🔴 КРИТИЧЕСКИЕ ПРОБЛЕМЫ (Приоритет 1)
### 1. Нарушение лимита строк - ИСПРАВЛЕНО ✅
- [x] `ConfigLoader.java`: **712 строк** → разбито на 4 класса ✅
- [x] `MineCommands.java`: **341 строк** → разбито на 4 класса ✅
- [x] `Mines.java`: **247→157 строк** → разбито на 3 сервиса ✅
- [x] `LoMinesPlugin.java`: **220→109 строк** → вынесено в ComponentInitializer ✅
- [x] `Mine.java`: **218 строк** → приемлемо ✅
### 2. Нарушение лимита файлов в папке (≤6) - ИСПРАВЛЕНО ✅
- [x] `data/`: **15 файлов** → разбито на подпапки ✅
- [x] `util/`: **9 файлов** → разбито на подпапки ✅
- [x] `core/`: **7→4 файлов** → создана подпапка service/ ✅
**Целевые лимиты:**
- Главный класс плагина: **≤100 строк**
- Обычные классы: **≤200 строк**
- Утилиты и хелперы: **≤150 строк**
- **Файлов в одной папке: ≤6**
---
## 🟡 НАРУШЕНИЯ ПРИНЦИПОВ (Приоритет 2) - ИСПРАВЛЕНО ✅
### KISS (Keep It Simple, Stupid) ✅
- [x] Упростить `ConfigLoader` — разбито на Parser, Validator, Serializer ✅
- [x] Упростить парсинг наград — используется RewardParser ✅
- [x] Убрать сложную логику из команд — вынесено в сервисы ✅
### DRY (Don't Repeat Yourself) ✅
- [x] Создать `ValidationUtils` для повторяющейся валидации Material ✅
- [x] Создать `ErrorHandler` для унифицированной обработки ошибок ✅
- [x] Создать `MineRepository` для устранения дублирования ✅
- [x] Унифицировать форматирование сообщений через MessageFormatter ✅
### SOLID ✅
- [x] **S**: ConfigLoader разделен на Parser, Validator, Serializer ✅
- [x] **S**: MineCommands разделен на Admin, Player, Stats, Mask команды ✅
- [x] **S**: Mines разделен на MineFileManager, MineRepository ✅
- [x] **I**: MineConfig с Builder pattern ✅
- [x] **D**: Repository pattern для доступа к данным ✅
---
## 🟢 УЛУЧШЕНИЯ АРХИТЕКТУРЫ (Приоритет 3)
### Новая структура пакетов ✅
```
dev.loki.lomines/
├── LoMinesPlugin.java (≤120 строк)
├── command/
│ ├── AdminCommands.java
│ ├── PlayerCommands.java
│ ├── StatsCommands.java
│ └── MaskCommands.java
├── core/
│ ├── Mine.java
│ ├── Mines.java
│ ├── MineTicker.java
│ └── service/
│ ├── MineFileManager.java
│ ├── MineRepository.java
│ └── MaskScanService.java
├── data/
│ ├── config/
│ │ ├── ConfigLoader.java
│ │ ├── ConfigValidator.java
│ │ ├── ConfigSerializer.java
│ │ ├── MineConfig.java
│ │ └── parser/
│ │ ├── ConfigParser.java
│ │ └── ConfigParseException.java
│ ├── reward/
│ │ ├── Reward.java
│ │ ├── RewardParser.java
│ │ └── RewardItemParser.java
│ └── stats/
│ ├── StatsManager.java
│ ├── PlayerStats.java
│ ├── Leaderboard.java
│ └── LeaderboardEntry.java
├── handler/
│ ├── ActionBarHandler.java
│ ├── MineBlockHandler.java
│ ├── MineResetHandler.java
│ └── MineRewardHandler.java
├── util/
│ ├── ValidationUtils.java
│ ├── ErrorHandler.java
│ ├── MessageFormatter.java
│ ├── format/
│ │ ├── ChunkUtils.java
│ │ └── TimeFormatter.java
│ ├── location/
│ │ ├── BlockKeys.java
│ │ ├── Cuboid.java
│ │ └── LocationParser.java
│ └── selection/
│ ├── MaskScanner.java
│ ├── Selection.java
│ └── SelectionManager.java
└── wand/
├── GroupWandItem.java
├── GroupWandManager.java
└── GroupWandSession.java
```
---
## 📦 Git и Инфраструктура
### Git Setup ✅
- [x] Инициализировать git репозиторий ✅
- [x] Обновить `.gitignore` ✅
- [x] Создать начальный коммит ✅
- [x] Рефакторинг пакетов com.loki -> dev.loki ✅
- [ ] Восстановить Gradle wrapper
### Правила коммитов
```
feat: новая функциональность
fix: исправление бага
refactor: рефакторинг без изменения функциональности
docs: изменения в документации
test: добавление/изменение тестов
chore: обновление зависимостей, конфигурации
```
---
## 🎯 Что делать дальше
### Приоритет 1: GUI редакторы (неделя 1)
1. [ ] Создать `MineEditGui` — редактирование существующей шахты
2. [ ] Создать `BlockConfigGui` — настройка блоков через GUI
3. [ ] Создать `RewardConfigGui` — настройка наград через GUI
4. [ ] Создать `ResetConfigGui` — настройка таймеров сброса
5. [ ] Создать `TeleportConfigGui` — настройка телепортации
### Приоритет 2: Интеграции (неделя 2)
1. [ ] Включить PlaceholderAPI — расширение для PAPI
2. [ ] Включить Oraxen — поддержка кастомных блоков
3. [ ] Включить ItemsAdder — поддержка кастомных блоков
4. [ ] Добавить Vault — экономика для наград
### Приоритет 3: Фичи (неделя 3)
1. [ ] Добавить `/lm info <mine>` — детальная информация о шахте
2. [ ] Добавить `/lm tp <mine>` — телепортация в шахту
3. [ ] Добавить `/lm copy <from> <to>` — копирование конфигурации
4. [ ] Добавить метрики — `/lm metrics` для админов
---
## 📝 Правила разработки
### Лимиты строк
- **Главный класс плагина:** ≤100 строк
- **Обычные классы:** ≤200 строк
- **Утилиты:** ≤150 строк
- **Тесты:** ≤300 строк
### Принципы
- **KISS:** Один метод = одна задача, вложенность ≤3 уровней
- **DRY:** Дублирование кода ≥3 раз → вынести в метод/класс
- **SOLID:** Каждый класс = одна ответственность
- **YAGNI:** Не добавлять функциональность "на будущее"
### Code Review Checklist
- [ ] Класс ≤200 строк (главный ≤100)
- [ ] Метод ≤30 строк
- [ ] Вложенность ≤3 уровней
- [ ] Нет дублирования кода
- [ ] Понятные имена переменных/методов
- [ ] Есть JavaDoc для публичных методов
- [ ] Есть тесты для новой функциональности
---
## 📊 Метрики качества
### Текущие
- Средний размер класса: **~180 строк** ✅
- Классов >200 строк: **~10%** ✅
- Дублирование кода: **<5%** ✅
- Покрытие тестами: **~50%** ⚠️
### Целевые
- Средний размер класса: **≤150 строк**
- Классов >200 строк: **0%**
- Дублирование кода: **<5%**
- Покрытие тестами: **≥70%**
---
## 🐛 Технический долг - РЕШЕНО
### 🔴 Критический - РЕШЕНО ✅
- [x] `ConfigLoader` — разбито на 4 класса ✅
- [x] `MineCommands` — разбито на 4 класса ✅
- [x] Импорты ConfigParseException в тестах — исправлено ✅
### 🟡 Средний
- [ ] Смешанные типы блоков в одной шахте
- [ ] `mine.blocks = mine.volume` выставляется до завершения fill
### 🟢 Низкий
- [ ] `ChunkUtils` — хрупкий `Class.forName` для Paper
- [ ] `SelectionWand.drawLine` — спам частиц для высоких шахт
---
## 🎯 Следующие шаги
1. ✅ Рефакторинг пакетов com.loki -> dev.loki (2026-05-31)
2. ✅ Исправление импортов в тестах (2026-05-31)
3. [ ] Восстановить Gradle wrapper
4. [ ] GUI редакторы для шахт
5. [ ] Включить интеграции (PlaceholderAPI, Oraxen, ItemsAdder)
6. [ ] Добавить новые фичи (/lm info, /lm tp, /lm copy)
7. [ ] Увеличить покрытие тестами до 70%
---
*Последнее обновление: 2026-05-31
Рефакторинг пакетов завершен: com.loki -> dev.loki*