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;
@ -9,27 +9,27 @@ import java.util.function.IntConsumer;
/** /**
* Abstract base class for block setting strategies. * Abstract base class for block setting strategies.
* Provides a template for filling mine regions with blocks. * Provides a template for filling mine regions with blocks.
* *
* <p>Implementations:</p> * <p>Implementations:</p>
* <ul> * <ul>
* <li>BukkitBlockSetter - For vanilla Minecraft blocks</li> * <li>BukkitBlockSetter - For vanilla Minecraft blocks</li>
* <li>OraxenBlockSetter - For Oraxen custom blocks</li> * <li>OraxenBlockSetter - For Oraxen custom blocks</li>
* <li>ItemsAdderBlockSetter - For ItemsAdder custom blocks</li> * <li>ItemsAdderBlockSetter - For ItemsAdder custom blocks</li>
* </ul> * </ul>
* *
* <p>This is a placeholder that will be fully implemented in task 2.7.</p> * <p>This is a placeholder that will be fully implemented in task 2.7.</p>
*/ */
public abstract class BlockSetter { 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);
/** /**
* Places random weighted blocks only at the given locations (mask mode). * Places random weighted blocks only at the given locations (mask mode).
*/ */

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;
@ -20,26 +19,26 @@ import java.util.function.IntConsumer;
* Uses Bukkit API to set blocks with optimal performance settings. * Uses Bukkit API to set blocks with optimal performance settings.
*/ */
public final class BukkitBlockSetter extends BlockSetter { public final class BukkitBlockSetter extends BlockSetter {
private final Map<String, Double> weights; private final Map<String, Double> weights;
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
public BukkitBlockSetter(Map<String, Double> weights, LoMinesPlugin plugin) { public BukkitBlockSetter(Map<String, Double> weights, LoMinesPlugin plugin) {
this.weights = weights; this.weights = weights;
this.plugin = plugin; this.plugin = plugin;
} }
@Override @Override
public void fill(Cuboid region, IntConsumer callback) { public void fill(Cuboid region, IntConsumer callback) {
Scheduler.get(plugin).runAsync(() -> { Scheduler.get(plugin).runAsync(() -> {
int count = fillSync(region); int count = fillSync(region);
Scheduler.get(plugin).run(() -> { Scheduler.get(plugin).run(() -> {
callback.accept(count); callback.accept(count);
}); });
}); });
} }
@Override @Override
public void fillAtLocations(List<Location> locations, IntConsumer callback) { public void fillAtLocations(List<Location> locations, IntConsumer callback) {
if (locations == null || locations.isEmpty()) { if (locations == null || locations.isEmpty()) {
@ -51,11 +50,11 @@ public final class BukkitBlockSetter extends BlockSetter {
Scheduler.get(plugin).run(() -> callback.accept(count)); Scheduler.get(plugin).run(() -> callback.accept(count));
}); });
} }
private int fillSync(Cuboid region) { private int fillSync(Cuboid region) {
int count = 0; int count = 0;
World world = region.getWorld(); World world = region.getWorld();
for (int x = region.getMinX(); x <= region.getMaxX(); x++) { for (int x = region.getMinX(); x <= region.getMaxX(); x++) {
for (int y = region.getMinY(); y <= region.getMaxY(); y++) { for (int y = region.getMinY(); y <= region.getMaxY(); y++) {
for (int z = region.getMinZ(); z <= region.getMaxZ(); z++) { for (int z = region.getMinZ(); z <= region.getMaxZ(); z++) {
@ -65,10 +64,10 @@ public final class BukkitBlockSetter extends BlockSetter {
} }
} }
} }
return count; return count;
} }
private int fillAtLocationsSync(List<Location> locations) { private int fillAtLocationsSync(List<Location> locations) {
int count = 0; int count = 0;
for (Location loc : locations) { for (Location loc : locations) {
@ -81,25 +80,25 @@ public final class BukkitBlockSetter extends BlockSetter {
} }
return count; return count;
} }
private BlockData sampleBlockData() { private BlockData sampleBlockData() {
// Simple weighted random selection // Simple weighted random selection
double totalWeight = weights.values().stream().mapToDouble(Double::doubleValue).sum(); double totalWeight = weights.values().stream().mapToDouble(Double::doubleValue).sum();
double random = Math.random() * totalWeight; double random = Math.random() * totalWeight;
double currentWeight = 0; double currentWeight = 0;
for (Map.Entry<String, Double> entry : weights.entrySet()) { for (Map.Entry<String, Double> entry : weights.entrySet()) {
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;
/** /**
@ -28,7 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* Thread-safe implementation using AtomicInteger for mutable state. * Thread-safe implementation using AtomicInteger for mutable state.
*/ */
public final class Mine { public final class Mine {
// Immutable configuration // Immutable configuration
private final String name; private final String name;
private final List<Cuboid> regions; private final List<Cuboid> regions;
@ -36,21 +31,23 @@ 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)
private final AtomicInteger blocks; private final AtomicInteger blocks;
private final AtomicInteger ticks; private final AtomicInteger ticks;
// Handlers // Handlers
private final MineResetHandler resetHandler; private final MineResetHandler resetHandler;
private final MineBlockHandler blockHandler; private final MineBlockHandler blockHandler;
private final ActionBarHandler actionBarHandler; private final ActionBarHandler actionBarHandler;
// Tasks // Tasks
private ScheduledTask actionBarTask; private ScheduledTask actionBarTask;
/** /**
* Creates a new mine instance. * Creates a new mine instance.
*/ */
@ -60,7 +57,7 @@ public final class Mine {
this.plugin = plugin; this.plugin = plugin;
this.regions = parseRegions(config); this.regions = parseRegions(config);
this.blockSetter = createBlockSetter(config, plugin); this.blockSetter = createBlockSetter(config, plugin);
if (config.getFillMode() == FillMode.MASK) { if (config.getFillMode() == FillMode.MASK) {
this.maskBlockKeys = new HashSet<>(); this.maskBlockKeys = new HashSet<>();
for (Location loc : config.getMaskPositions()) { for (Location loc : config.getMaskPositions()) {
@ -73,19 +70,19 @@ public final class Mine {
} }
this.blocks = new AtomicInteger(Math.max(0, totalVolume)); this.blocks = new AtomicInteger(Math.max(0, totalVolume));
this.ticks = new AtomicInteger(0); this.ticks = new AtomicInteger(0);
this.resetHandler = new MineResetHandler(this, plugin); this.resetHandler = new MineResetHandler(this, plugin);
this.blockHandler = new MineBlockHandler(this, plugin); this.blockHandler = new MineBlockHandler(this, plugin);
this.actionBarHandler = new ActionBarHandler(this); this.actionBarHandler = new ActionBarHandler(this);
} }
/** /**
* Starts periodic tasks for this mine. * Starts periodic tasks for this mine.
*/ */
public void start() { public void start() {
startActionBarTask(); startActionBarTask();
} }
/** /**
* Stops all tasks for this mine. * Stops all tasks for this mine.
*/ */
@ -95,28 +92,28 @@ public final class Mine {
actionBarTask = null; actionBarTask = null;
} }
} }
/** /**
* Resets the mine by filling all regions with blocks. * Resets the mine by filling all regions with blocks.
*/ */
public void reset(boolean silent) { public void reset(boolean silent) {
resetHandler.reset(silent); resetHandler.reset(silent);
} }
/** /**
* Handles a block break event in this mine. * Handles a block break event in this mine.
*/ */
public void onBlockBreak(Player player, Block block) { public void onBlockBreak(Player player, Block block) {
blockHandler.handle(player, block); blockHandler.handle(player, block);
} }
/** /**
* Checks if the given location is within any region of this mine. * Checks if the given location is within any region of this mine.
*/ */
public boolean contains(Location location) { public boolean contains(Location location) {
return regions.stream().anyMatch(region -> region.contains(location)); return regions.stream().anyMatch(region -> region.contains(location));
} }
/** /**
* Whether breaking this block counts toward mine progress ({@link FillMode#MASK} ignores blocks outside painted cells). * Whether breaking this block counts toward mine progress ({@link FillMode#MASK} ignores blocks outside painted cells).
*/ */
@ -129,14 +126,14 @@ public final class Mine {
} }
return maskBlockKeys.contains(BlockKeys.key(location)); return maskBlockKeys.contains(BlockKeys.key(location));
} }
/** /**
* Calculates the total volume of all regions in this mine. * Calculates the total volume of all regions in this mine.
*/ */
public int getTotalVolume() { public int getTotalVolume() {
return totalVolume; return totalVolume;
} }
/** /**
* Calculates the percentage of blocks remaining in the mine. * Calculates the percentage of blocks remaining in the mine.
*/ */
@ -146,54 +143,56 @@ public final class Mine {
} }
return (double) blocks.get() / totalVolume * 100.0; return (double) blocks.get() / totalVolume * 100.0;
} }
/** /**
* Parses cuboid regions from selection points in the config. * Parses cuboid regions from selection points in the config.
* Selections are paired: 1-2, 3-4, 5-6, 7-8, 9-10. * Selections are paired: 1-2, 3-4, 5-6, 7-8, 9-10.
*/ */
private List<Cuboid> parseRegions(MineConfig config) { private List<Cuboid> parseRegions(MineConfig config) {
List<Location> selections = config.getSelections(); List<Location> selections = config.getSelections();
if (selections.isEmpty()) { if (selections.isEmpty()) {
throw new IllegalArgumentException("Mine must have at least one region (2 selection points)"); throw new IllegalArgumentException("Mine must have at least one region (2 selection points)");
} }
if (selections.size() % 2 != 0) { if (selections.size() % 2 != 0) {
throw new IllegalArgumentException("Selections must be in pairs (even number of points)"); throw new IllegalArgumentException("Selections must be in pairs (even number of points)");
} }
List<Cuboid> cuboids = new ArrayList<>(); List<Cuboid> cuboids = new ArrayList<>();
for (int i = 0; i < selections.size(); i += 2) { for (int i = 0; i < selections.size(); i += 2) {
Location loc1 = selections.get(i); Location loc1 = selections.get(i);
Location loc2 = selections.get(i + 1); Location loc2 = selections.get(i + 1);
cuboids.add(new Cuboid(loc1, loc2)); cuboids.add(new Cuboid(loc1, loc2));
} }
return Collections.unmodifiableList(cuboids); return Collections.unmodifiableList(cuboids);
} }
/** /**
* Creates the appropriate BlockSetter based on block configuration. * Creates the appropriate BlockSetter based on block configuration.
*/ */
private BlockSetter createBlockSetter(MineConfig config, LoMinesPlugin plugin) { private BlockSetter createBlockSetter(MineConfig config, LoMinesPlugin plugin) {
Map<String, Double> blocks = config.getBlocks(); Map<String, Double> blocks = config.getBlocks();
if (blocks.isEmpty()) { if (blocks.isEmpty()) {
throw new IllegalArgumentException("Mine must have at least one block type"); throw new IllegalArgumentException("Mine must have at least one block type");
} }
// Get the first block key to determine the setter type // Get the first block key to determine the setter type
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);
} }
} }
/** /**
* Starts the action bar update task. * Starts the action bar update task.
*/ */
@ -201,18 +200,41 @@ public final class Mine {
if (!config.isActionBarEnabled()) { if (!config.isActionBarEnabled()) {
return; return;
} }
actionBarTask = Scheduler.get(plugin).runTimer(() -> { actionBarTask = Scheduler.get(plugin).runTimer(() -> {
actionBarHandler.sendToNearbyPlayers(); actionBarHandler.sendToNearbyPlayers();
}, 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.
@ -10,16 +10,16 @@ import dev.lolib.scheduler.ScheduledTask;
* This is more efficient than having each mine run its own ticker. * This is more efficient than having each mine run its own ticker.
*/ */
public final class MineTicker { public final class MineTicker {
private final Mines mines; private final Mines mines;
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private ScheduledTask tickerTask; private ScheduledTask tickerTask;
public MineTicker(Mines mines, LoMinesPlugin plugin) { public MineTicker(Mines mines, LoMinesPlugin plugin) {
this.mines = mines; this.mines = mines;
this.plugin = plugin; this.plugin = plugin;
} }
/** /**
* Starts the centralized ticker. * Starts the centralized ticker.
* Ticks all mines every game tick (1L, 1L). * Ticks all mines every game tick (1L, 1L).
@ -28,14 +28,14 @@ public final class MineTicker {
if (tickerTask != null) { if (tickerTask != null) {
return; // Already running return; // Already running
} }
tickerTask = Scheduler.get(plugin).runTimer(() -> { tickerTask = Scheduler.get(plugin).runTimer(() -> {
for (Mine mine : mines.getAll()) { for (Mine mine : mines.getAll()) {
tickMine(mine); tickMine(mine);
} }
}, 1L, 1L); }, 1L, 1L);
} }
/** /**
* Stops the centralized ticker. * Stops the centralized ticker.
*/ */
@ -45,13 +45,13 @@ public final class MineTicker {
tickerTask = null; tickerTask = null;
} }
} }
/** /**
* Ticks a single mine, checking if it needs to reset. * Ticks a single mine, checking if it needs to reset.
*/ */
private void tickMine(Mine mine) { private void tickMine(Mine mine) {
int currentTicks = mine.getTicksAtomic().incrementAndGet(); int currentTicks = mine.getTicksAtomic().incrementAndGet();
if (currentTicks >= mine.getConfig().getResetTicks()) { if (currentTicks >= mine.getConfig().getResetTicks()) {
mine.getTicksAtomic().set(0); mine.getTicksAtomic().set(0);
mine.reset(false); mine.reset(false);

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
@ -76,79 +83,79 @@ public final class MineConfig {
public static MineConfigBuilder builder() { public static MineConfigBuilder builder() {
return new MineConfigBuilder(); return new MineConfigBuilder();
} }
// Getters // Getters
public List<Location> getSelections() { public List<Location> getSelections() {
return selections; return selections;
} }
public Map<String, Double> getBlocks() { public Map<String, Double> getBlocks() {
return blocks; return blocks;
} }
public FillMode getFillMode() { public FillMode getFillMode() {
return fillMode; return fillMode;
} }
/** /**
* Material name for scanning marker blocks (vanilla), e.g. {@code pink_concrete}. * Material name for scanning marker blocks (vanilla), e.g. {@code pink_concrete}.
*/ */
public String getMaskMarkerMaterial() { public String getMaskMarkerMaterial() {
return maskMarkerMaterial; return maskMarkerMaterial;
} }
public List<Location> getMaskPositions() { public List<Location> getMaskPositions() {
return maskPositions; return maskPositions;
} }
public List<Reward> getRewards() { public List<Reward> getRewards() {
return rewards; return rewards;
} }
public int getResetTicks() { public int getResetTicks() {
return resetTicks; return resetTicks;
} }
public double getResetPercent() { public double getResetPercent() {
return resetPercent; return resetPercent;
} }
public boolean isResetOnPercentEnabled() { public boolean isResetOnPercentEnabled() {
return resetOnPercentEnabled; return resetOnPercentEnabled;
} }
public List<String> getResetCommands() { public List<String> getResetCommands() {
return resetCommands; return resetCommands;
} }
public String getBroadcastReset() { public String getBroadcastReset() {
return broadcastReset; return broadcastReset;
} }
public boolean isTeleportOnReset() { public boolean isTeleportOnReset() {
return teleportOnReset; return teleportOnReset;
} }
public Location getTeleportLocation() { public Location getTeleportLocation() {
return teleportLocation; return teleportLocation;
} }
public boolean isActionBarEnabled() { public boolean isActionBarEnabled() {
return actionBarEnabled; return actionBarEnabled;
} }
public String getActionBarMessage() { public String getActionBarMessage() {
return actionBarMessage; return actionBarMessage;
} }
public double getActionBarRange() { public double getActionBarRange() {
return actionBarRange; return actionBarRange;
} }
public String getTimerFormat() { public String getTimerFormat() {
return timerFormat; return timerFormat;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -172,7 +179,7 @@ public final class MineConfig {
Objects.equals(actionBarMessage, that.actionBarMessage) && Objects.equals(actionBarMessage, that.actionBarMessage) &&
Objects.equals(timerFormat, that.timerFormat); Objects.equals(timerFormat, that.timerFormat);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(selections, blocks, fillMode, maskMarkerMaterial, maskPositions, rewards, resetTicks, resetPercent, return Objects.hash(selections, blocks, fillMode, maskMarkerMaterial, maskPositions, rewards, resetTicks, resetPercent,

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,15 +1,15 @@
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.
* Provides descriptive error messages for invalid configuration data. * Provides descriptive error messages for invalid configuration data.
*/ */
public final class ConfigParseException extends Exception { public final class ConfigParseException extends Exception {
public ConfigParseException(String message) { public ConfigParseException(String message) {
super(message); super(message);
} }
public ConfigParseException(String message, Throwable cause) { public ConfigParseException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

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,15 +10,10 @@ 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; public Reward(List<Material> materials, double chance,
private final List<ItemStack> items; List<ItemStack> items, List<String> commands,
private final List<String> commands;
private final boolean preventDrops;
public Reward(List<Material> materials, double chance,
List<ItemStack> items, List<String> commands,
boolean preventDrops) { boolean preventDrops) {
this.materials = List.copyOf(materials); this.materials = List.copyOf(materials);
this.chance = chance; this.chance = chance;
@ -26,31 +21,32 @@ public final class Reward {
this.commands = List.copyOf(commands); this.commands = List.copyOf(commands);
this.preventDrops = preventDrops; this.preventDrops = preventDrops;
} }
public boolean matches(Material material) { public boolean matches(Material material) {
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;
} }
public double getChance() { public double getChance() {
return chance; return chance;
} }
public List<ItemStack> getItems() { public List<ItemStack> getItems() {
return items; return items;
} }
public List<String> getCommands() { public List<String> getCommands() {
return commands; return commands;
} }
public boolean isPreventDrops() { public boolean isPreventDrops() {
return preventDrops; return preventDrops;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -62,9 +58,5 @@ public final class Reward {
Objects.equals(items, reward.items) && Objects.equals(items, reward.items) &&
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,27 +8,26 @@ 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.
* *
* @param statsManager The stats manager to retrieve player statistics from * @param statsManager The stats manager to retrieve player statistics from
*/ */
public Leaderboard(StatsManager statsManager) { public Leaderboard(StatsManager statsManager) {
this.statsManager = statsManager; this.statsManager = statsManager;
} }
/** /**
* Invalidates the cached top total leaderboard. * Invalidates the cached top total leaderboard.
* Should be called when player statistics change. * Should be called when player statistics change.
@ -38,11 +37,11 @@ public final class Leaderboard {
cachedTopTotal = null; cachedTopTotal = null;
} }
} }
/** /**
* Returns the top players by total blocks mined across all mines. * Returns the top players by total blocks mined across all mines.
* Results are cached and reused until invalidateCache() is called. * Results are cached and reused until invalidateCache() is called.
* *
* @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 * @return List of leaderboard entries sorted by descending block count
*/ */
@ -51,52 +50,52 @@ public final class Leaderboard {
if (cachedTopTotal != null) { if (cachedTopTotal != null) {
return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size())); return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size()));
} }
// Slow path: build cache with double-checked locking // Slow path: build cache with double-checked locking
synchronized (cacheLock) { synchronized (cacheLock) {
// Double-check: another thread might have built the cache // Double-check: another thread might have built the cache
if (cachedTopTotal != null) { if (cachedTopTotal != null) {
return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size())); return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size()));
} }
// 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()));
} }
} }
/** /**
* Returns the top players for a specific mine. * Returns the top players for a specific mine.
* 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());
} }
/** /**
* Returns the position of a player in the total leaderboard. * Returns the position of a player in the total leaderboard.
* *
* @param playerId The UUID of the player * @param playerId The UUID of the player
* @return The 1-based position of the player, or -1 if the player is not in the leaderboard * @return The 1-based position of the player, or -1 if the player is not in the 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;
@ -32,12 +21,7 @@ public final class LeaderboardEntry {
LeaderboardEntry that = (LeaderboardEntry) o; LeaderboardEntry that = (LeaderboardEntry) o;
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;
@ -9,46 +9,46 @@ import java.util.concurrent.atomic.AtomicLong;
* This is a placeholder implementation that will be completed in task 5.1. * This is a placeholder implementation that will be completed in task 5.1.
*/ */
public final class PlayerStats { public final class PlayerStats {
private final UUID playerId; private final UUID playerId;
private final AtomicLong totalBlocks = new AtomicLong(0); private final AtomicLong totalBlocks = new AtomicLong(0);
private final ConcurrentHashMap<String, AtomicLong> mineStats = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicLong> mineStats = new ConcurrentHashMap<>();
public PlayerStats(UUID playerId) { public PlayerStats(UUID playerId) {
this.playerId = playerId; this.playerId = playerId;
} }
public UUID getPlayerId() { public UUID getPlayerId() {
return playerId; return playerId;
} }
public long getTotalBlocks() { public long getTotalBlocks() {
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;
} }
public void incrementTotal() { public void incrementTotal() {
totalBlocks.incrementAndGet(); totalBlocks.incrementAndGet();
} }
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() {
java.util.Map<String, Long> snapshot = new java.util.HashMap<>(); java.util.Map<String, Long> snapshot = new java.util.HashMap<>();
mineStats.forEach((mine, counter) -> snapshot.put(mine, counter.get())); mineStats.forEach((mine, counter) -> snapshot.put(mine, counter.get()));

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;
@ -21,19 +21,19 @@ import java.util.concurrent.ConcurrentHashMap;
* Handles loading, saving, and auto-saving of player statistics. * Handles loading, saving, and auto-saving of player statistics.
*/ */
public final class StatsManager { public final class StatsManager {
private final Map<UUID, PlayerStats> stats = new ConcurrentHashMap<>(); private final Map<UUID, PlayerStats> stats = new ConcurrentHashMap<>();
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Path statsFile; private final Path statsFile;
private final Leaderboard leaderboard; private final Leaderboard leaderboard;
private ScheduledTask saveTask; private ScheduledTask saveTask;
public StatsManager(LoMinesPlugin plugin) { public StatsManager(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml"); this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml");
this.leaderboard = new Leaderboard(this); this.leaderboard = new Leaderboard(this);
} }
/** /**
* Loads statistics from file. * Loads statistics from file.
*/ */
@ -42,31 +42,31 @@ public final class StatsManager {
plugin.loLogger().info("Stats file does not exist, starting with empty statistics"); plugin.loLogger().info("Stats file does not exist, starting with empty statistics");
return; return;
} }
File file = statsFile.toFile(); File file = statsFile.toFile();
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file); YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
ConfigurationSection playersSection = yaml.getConfigurationSection("players"); ConfigurationSection playersSection = yaml.getConfigurationSection("players");
if (playersSection == null) { if (playersSection == null) {
plugin.loLogger().info("No players section found in stats.yml"); plugin.loLogger().info("No players section found in stats.yml");
return; return;
} }
for (String uuidString : playersSection.getKeys(false)) { for (String uuidString : playersSection.getKeys(false)) {
try { try {
UUID playerId = UUID.fromString(uuidString); UUID playerId = UUID.fromString(uuidString);
ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString); ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString);
if (playerSection == null) { if (playerSection == null) {
continue; continue;
} }
PlayerStats playerStats = getOrCreate(playerId); PlayerStats playerStats = getOrCreate(playerId);
// Load total blocks // Load total blocks
long totalBlocks = playerSection.getLong("total", 0); long totalBlocks = playerSection.getLong("total", 0);
playerStats.setTotalBlocks(totalBlocks); playerStats.setTotalBlocks(totalBlocks);
// Load mine-specific stats // Load mine-specific stats
ConfigurationSection minesSection = playerSection.getConfigurationSection("mines"); ConfigurationSection minesSection = playerSection.getConfigurationSection("mines");
if (minesSection != null) { if (minesSection != null) {
@ -79,25 +79,25 @@ public final class StatsManager {
plugin.loLogger().warn("Invalid UUID in stats.yml: " + uuidString); plugin.loLogger().warn("Invalid UUID in stats.yml: " + uuidString);
} }
} }
plugin.loLogger().info("Loaded statistics for " + stats.size() + " player(s)"); plugin.loLogger().info("Loaded statistics for " + stats.size() + " player(s)");
} }
/** /**
* Saves statistics to file. * Saves statistics to file.
*/ */
public void save() throws IOException { public void save() throws IOException {
YamlConfiguration yaml = new YamlConfiguration(); YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, PlayerStats> entry : stats.entrySet()) { for (Map.Entry<UUID, PlayerStats> entry : stats.entrySet()) {
UUID playerId = entry.getKey(); UUID playerId = entry.getKey();
PlayerStats playerStats = entry.getValue(); PlayerStats playerStats = entry.getValue();
String path = "players." + playerId.toString(); String path = "players." + playerId.toString();
// Save total blocks // Save total blocks
yaml.set(path + ".total", playerStats.getTotalBlocks()); yaml.set(path + ".total", playerStats.getTotalBlocks());
// Save mine-specific stats // Save mine-specific stats
Map<String, Long> mineStats = playerStats.getMineStatsSnapshot(); Map<String, Long> mineStats = playerStats.getMineStatsSnapshot();
if (!mineStats.isEmpty()) { if (!mineStats.isEmpty()) {
@ -106,36 +106,36 @@ public final class StatsManager {
} }
} }
} }
// Ensure parent directory exists // Ensure parent directory exists
if (!Files.exists(statsFile.getParent())) { if (!Files.exists(statsFile.getParent())) {
Files.createDirectories(statsFile.getParent()); Files.createDirectories(statsFile.getParent());
} }
yaml.save(statsFile.toFile()); yaml.save(statsFile.toFile());
} }
/** /**
* Starts auto-save task. * Starts auto-save task.
*/ */
public void startAutoSave() { public void startAutoSave() {
// Auto-save every 5 minutes (20 ticks/second * 60 seconds * 5 minutes) // Auto-save every 5 minutes (20 ticks/second * 60 seconds * 5 minutes)
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
); );
} }
/** /**
* Stops auto-save task. * Stops auto-save task.
*/ */
@ -145,14 +145,14 @@ public final class StatsManager {
saveTask = null; saveTask = null;
} }
} }
/** /**
* Gets or creates player statistics. * Gets or creates player statistics.
*/ */
public PlayerStats getOrCreate(UUID playerId) { public PlayerStats getOrCreate(UUID playerId) {
return stats.computeIfAbsent(playerId, PlayerStats::new); return stats.computeIfAbsent(playerId, PlayerStats::new);
} }
/** /**
* Increments block count for a player in a specific mine. * Increments block count for a player in a specific mine.
*/ */
@ -160,18 +160,18 @@ public final class StatsManager {
PlayerStats playerStats = getOrCreate(playerId); PlayerStats playerStats = getOrCreate(playerId);
playerStats.incrementTotal(); playerStats.incrementTotal();
playerStats.incrementMine(mineName); playerStats.incrementMine(mineName);
// Invalidate leaderboard cache // Invalidate leaderboard cache
leaderboard.invalidateCache(); leaderboard.invalidateCache();
} }
/** /**
* Returns all player statistics. * Returns all player statistics.
*/ */
public Collection<PlayerStats> getAllStats() { public Collection<PlayerStats> getAllStats() {
return Collections.unmodifiableCollection(stats.values()); return Collections.unmodifiableCollection(stats.values());
} }
/** /**
* Returns the leaderboard instance. * Returns the leaderboard instance.
*/ */

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;
@ -20,31 +20,37 @@ import java.util.List;
* 6-row chest: left column = create; center 3×3 = nine sub-mines with shared settings. * 6-row chest: left column = create; center 3×3 = nine sub-mines with shared settings.
*/ */
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() {
} }
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);
} }
public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv) { public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv) {
fill(plugin, player, inv); fill(plugin, player, inv);
} }
private static void fill(LoMinesPlugin plugin, Player player, Inventory inv) { private static void fill(LoMinesPlugin plugin, Player player, Inventory inv) {
GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId());
for (int i = 0; i < SIZE; i++) { for (int i = 0; i < SIZE; i++) {
@ -56,7 +62,7 @@ public final class GroupCreateGui {
inv.setItem(MINE_SLOTS[i], mineSlotItem(session, i)); inv.setItem(MINE_SLOTS[i], mineSlotItem(session, i));
} }
} }
private static ItemStack filler() { private static ItemStack filler() {
ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = pane.getItemMeta(); ItemMeta meta = pane.getItemMeta();
@ -66,21 +72,21 @@ public final class GroupCreateGui {
} }
return pane; return pane;
} }
private static ItemStack createButton() { private static ItemStack createButton() {
ItemStack stack = new ItemStack(Material.LIME_CONCRETE); ItemStack stack = new ItemStack(Material.LIME_CONCRETE);
ItemMeta meta = stack.getItemMeta(); ItemMeta meta = stack.getItemMeta();
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);
} }
return stack; return stack;
} }
private static ItemStack infoPaper(GroupWandSession session) { private static ItemStack infoPaper(GroupWandSession session) {
ItemStack stack = new ItemStack(Material.PAPER); ItemStack stack = new ItemStack(Material.PAPER);
ItemMeta meta = stack.getItemMeta(); ItemMeta meta = stack.getItemMeta();
@ -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));
@ -99,7 +105,7 @@ public final class GroupCreateGui {
} }
return stack; return stack;
} }
private static ItemStack mineSlotItem(GroupWandSession session, int index) { private static ItemStack mineSlotItem(GroupWandSession session, int index) {
boolean active = session.getActiveSlot() == index; boolean active = session.getActiveSlot() == index;
boolean ready = session.isSlotReady(index); boolean ready = session.isSlotReady(index);
@ -126,14 +132,14 @@ 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);
} }
return stack; return stack;
} }
public static int mineSlotIndex(int rawSlot) { public static int mineSlotIndex(int rawSlot) {
for (int i = 0; i < MINE_SLOTS.length; i++) { for (int i = 0; i < MINE_SLOTS.length; i++) {
if (MINE_SLOTS[i] == rawSlot) { if (MINE_SLOTS[i] == rawSlot) {
@ -142,7 +148,7 @@ public final class GroupCreateGui {
} }
return -1; return -1;
} }
/** /**
* @return true if handled (caller should cancel event) * @return true if handled (caller should cancel event)
*/ */
@ -169,7 +175,7 @@ public final class GroupCreateGui {
} }
return true; return true;
} }
private static void runCreate(LoMinesPlugin plugin, Player player, GroupWandSession session) { private static void runCreate(LoMinesPlugin plugin, Player player, GroupWandSession session) {
if (!session.hasBaseName()) { if (!session.hasBaseName()) {
player.sendMessage(Component.text("Сначала: /lm group <префикс>", NamedTextColor.RED)); player.sendMessage(Component.text("Сначала: /lm group <префикс>", NamedTextColor.RED));
@ -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;
@ -9,24 +9,24 @@ import java.util.UUID;
* Marks the group-create menu and ties it to a player. * Marks the group-create menu and ties it to a player.
*/ */
public final class GroupCreateGuiHolder implements InventoryHolder { public final class GroupCreateGuiHolder implements InventoryHolder {
private final UUID playerId; private final UUID playerId;
private Inventory inventory; private Inventory inventory;
public GroupCreateGuiHolder(UUID playerId) { public GroupCreateGuiHolder(UUID playerId) {
this.playerId = playerId; this.playerId = playerId;
} }
public UUID getPlayerId() { public UUID getPlayerId() {
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;
@ -12,13 +12,13 @@ import org.bukkit.entity.Player;
* Sends formatted messages to players within range of the mine. * Sends formatted messages to players within range of the mine.
*/ */
public final class ActionBarHandler { public final class ActionBarHandler {
private final Mine mine; private final Mine mine;
public ActionBarHandler(Mine mine) { public ActionBarHandler(Mine mine) {
this.mine = mine; this.mine = mine;
} }
/** /**
* Sends action bar messages to all players within range of the mine. * Sends action bar messages to all players within range of the mine.
*/ */
@ -26,61 +26,61 @@ public final class ActionBarHandler {
if (!mine.getConfig().isActionBarEnabled()) { if (!mine.getConfig().isActionBarEnabled()) {
return; return;
} }
Location center = calculateCenter(); Location center = calculateCenter();
if (center == null || center.getWorld() == null) { if (center == null || center.getWorld() == null) {
return; return;
} }
double range = mine.getConfig().getActionBarRange(); double range = mine.getConfig().getActionBarRange();
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));
} }
/** /**
* Calculates the center point of the mine. * Calculates the center point of the mine.
* Uses the center of the first region. * Uses the center of the first region.
* *
* @return The center location, or null if no regions exist * @return The center location, or null if no regions exist
*/ */
private Location calculateCenter() { private Location calculateCenter() {
if (mine.getRegions().isEmpty()) { if (mine.getRegions().isEmpty()) {
return null; return null;
} }
Cuboid firstRegion = mine.getRegions().get(0); Cuboid firstRegion = mine.getRegions().get(0);
int centerX = (firstRegion.getMinX() + firstRegion.getMaxX()) / 2; int centerX = (firstRegion.getMinX() + firstRegion.getMaxX()) / 2;
int centerY = (firstRegion.getMinY() + firstRegion.getMaxY()) / 2; int centerY = (firstRegion.getMinY() + firstRegion.getMaxY()) / 2;
int centerZ = (firstRegion.getMinZ() + firstRegion.getMaxZ()) / 2; int centerZ = (firstRegion.getMinZ() + firstRegion.getMaxZ()) / 2;
return new Location(firstRegion.getWorld(), centerX, centerY, centerZ); return new Location(firstRegion.getWorld(), centerX, centerY, centerZ);
} }
/** /**
* Formats the action bar message with placeholders. * Formats the action bar message with placeholders.
* *
* @return The formatted message * @return The formatted message
*/ */
private String formatMessage() { private String formatMessage() {
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;
} }
/** /**
* Formats the time remaining until reset. * Formats the time remaining until reset.
* *
* @return Formatted time string * @return Formatted time string
*/ */
private String formatTime() { private String formatTime() {

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;
@ -11,35 +11,35 @@ import org.bukkit.entity.Player;
* Coordinates reward checking, statistics updates, and reset condition checks. * Coordinates reward checking, statistics updates, and reset condition checks.
*/ */
public final class MineBlockHandler { public final class MineBlockHandler {
private final Mine mine; private final Mine mine;
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final MineRewardHandler rewardHandler; private final MineRewardHandler rewardHandler;
public MineBlockHandler(Mine mine, LoMinesPlugin plugin) { public MineBlockHandler(Mine mine, LoMinesPlugin plugin) {
this.mine = mine; this.mine = mine;
this.plugin = plugin; this.plugin = plugin;
this.rewardHandler = new MineRewardHandler(mine.getConfig(), plugin); this.rewardHandler = new MineRewardHandler(mine.getConfig(), plugin);
} }
/** /**
* 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();
rewardHandler.checkRewards(player, block); rewardHandler.checkRewards(player, block);
if (plugin.getConfig().getBoolean("statistics-enabled", true)) { if (plugin.getConfig().getBoolean("statistics-enabled", true)) {
plugin.getStatsManager().incrementBlocks(player.getUniqueId(), mine.getName()); plugin.getStatsManager().incrementBlocks(player.getUniqueId(), mine.getName());
} }
checkResetConditions(remaining); checkResetConditions(remaining);
} }
/** /**
* Checks if reset conditions are met and triggers reset if needed. * Checks if reset conditions are met and triggers reset if needed.
*/ */
@ -50,7 +50,7 @@ public final class MineBlockHandler {
return; return;
} }
double percent = (double) remaining / total * 100.0; double percent = (double) remaining / total * 100.0;
if (config.isResetOnPercentEnabled() && percent <= config.getResetPercent()) { if (config.isResetOnPercentEnabled() && percent <= config.getResetPercent()) {
mine.reset(false); mine.reset(false);
} }

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;
@ -18,7 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Handles mine reset operations. * Handles mine reset operations.
* Ensures thread-safe reset execution and prevents concurrent resets. * Ensures thread-safe reset execution and prevents concurrent resets.
* *
* <p>Reset process:</p> * <p>Reset process:</p>
* <ol> * <ol>
* <li>Check if reset is already running (atomic check)</li> * <li>Check if reset is already running (atomic check)</li>
@ -28,27 +27,27 @@ import java.util.concurrent.atomic.AtomicInteger;
* </ol> * </ol>
*/ */
public final class MineResetHandler { public final class MineResetHandler {
private final Mine mine; private final Mine mine;
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false);
public MineResetHandler(Mine mine, LoMinesPlugin plugin) { public MineResetHandler(Mine mine, LoMinesPlugin plugin) {
this.mine = mine; this.mine = mine;
this.plugin = plugin; this.plugin = plugin;
} }
/** /**
* Resets the mine by filling all regions with blocks. * Resets the mine by filling all regions with blocks.
* If a reset is already in progress, this call is ignored. * If a reset is already in progress, this call is ignored.
* *
* @param silent If true, suppresses broadcast messages * @param silent If true, suppresses broadcast messages
*/ */
public void reset(boolean silent) { public void reset(boolean silent) {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {
return; return;
} }
try { try {
resetAllRegions(silent); resetAllRegions(silent);
} catch (Exception e) { } catch (Exception e) {
@ -57,7 +56,7 @@ public final class MineResetHandler {
e.printStackTrace(); e.printStackTrace();
} }
} }
/** /**
* Fills all regions asynchronously and coordinates completion. * Fills all regions asynchronously and coordinates completion.
*/ */
@ -65,23 +64,23 @@ public final class MineResetHandler {
List<Cuboid> regions = mine.getRegions(); List<Cuboid> regions = mine.getRegions();
AtomicInteger completed = new AtomicInteger(0); AtomicInteger completed = new AtomicInteger(0);
AtomicInteger totalBlocks = new AtomicInteger(0); AtomicInteger totalBlocks = new AtomicInteger(0);
if (mine.getBlockSetter() == null) { if (mine.getBlockSetter() == null) {
plugin.loLogger().warn("BlockSetter not initialized for mine " + mine.getName()); plugin.loLogger().warn("BlockSetter not initialized for mine " + mine.getName());
running.set(false); running.set(false);
return; return;
} }
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;
} }
for (Cuboid region : regions) { for (Cuboid region : regions) {
mine.getBlockSetter().fill(region, blocksSet -> { mine.getBlockSetter().fill(region, blocksSet -> {
totalBlocks.addAndGet(blocksSet); totalBlocks.addAndGet(blocksSet);
if (completed.incrementAndGet() == regions.size()) { if (completed.incrementAndGet() == regions.size()) {
Scheduler.get(plugin).run(() -> { Scheduler.get(plugin).run(() -> {
onResetComplete(totalBlocks.get(), silent); onResetComplete(totalBlocks.get(), silent);
@ -90,7 +89,7 @@ public final class MineResetHandler {
}); });
} }
} }
/** /**
* Called when all regions have been filled. * Called when all regions have been filled.
* Executes post-reset actions in the main thread. * Executes post-reset actions in the main thread.
@ -98,20 +97,20 @@ public final class MineResetHandler {
private void onResetComplete(int totalBlocks, boolean silent) { private void onResetComplete(int totalBlocks, boolean silent) {
mine.getBlocksAtomic().set(totalBlocks); mine.getBlocksAtomic().set(totalBlocks);
mine.getTicksAtomic().set(0); mine.getTicksAtomic().set(0);
executeResetCommands(); executeResetCommands();
if (!silent) { if (!silent) {
broadcastReset(); broadcastReset();
} }
if (mine.getConfig().isTeleportOnReset()) { if (mine.getConfig().isTeleportOnReset()) {
teleportPlayers(); teleportPlayers();
} }
running.set(false); running.set(false);
} }
/** /**
* Executes commands configured for reset. * Executes commands configured for reset.
*/ */
@ -122,7 +121,7 @@ public final class MineResetHandler {
plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), parsed); plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), parsed);
} }
} }
/** /**
* Broadcasts reset message to players. * Broadcasts reset message to players.
*/ */
@ -134,7 +133,7 @@ public final class MineResetHandler {
plugin.getServer().broadcast(component); plugin.getServer().broadcast(component);
} }
} }
/** /**
* Teleports players standing inside the mine to the configured location. * Teleports players standing inside the mine to the configured location.
*/ */

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;
@ -16,54 +16,54 @@ import java.util.Random;
* Checks configured rewards and gives items/executes commands based on chance. * Checks configured rewards and gives items/executes commands based on chance.
*/ */
public final class MineRewardHandler { public final class MineRewardHandler {
private final List<Reward> rewards; private final List<Reward> rewards;
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Random random = new Random(); private final Random random = new Random();
public MineRewardHandler(MineConfig config, LoMinesPlugin plugin) { public MineRewardHandler(MineConfig config, LoMinesPlugin plugin) {
this.rewards = config.getRewards(); this.rewards = config.getRewards();
this.plugin = plugin; this.plugin = plugin;
} }
/** /**
* 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);
} }
} }
} }
/** /**
* Rolls a random chance check. * Rolls a random chance check.
* *
* @param chance The chance percentage (0-100) * @param chance The chance percentage (0-100)
* @return true if the roll succeeds * @return true if the roll succeeds
*/ */
private boolean rollChance(double chance) { private boolean rollChance(double chance) {
return random.nextDouble() * 100.0 < chance; return random.nextDouble() * 100.0 < chance;
} }
/** /**
* Gives a reward to the player. * Gives a reward to the player.
* *
* @param player The player to give the reward to * @param player The player to give the reward to
* @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;
@ -14,17 +14,17 @@ import java.util.Optional;
* This is a placeholder implementation that will be completed in task 11.1. * This is a placeholder implementation that will be completed in task 11.1.
*/ */
public final class BlockBreakListener implements Listener { public final class BlockBreakListener implements Listener {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
public BlockBreakListener(LoMinesPlugin plugin) { public BlockBreakListener(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) { public void onBlockBreak(BlockBreakEvent event) {
Optional<Mine> mine = plugin.getMines().findByLocation(event.getBlock().getLocation()); Optional<Mine> mine = plugin.getMines().findByLocation(event.getBlock().getLocation());
if (mine.isPresent()) { if (mine.isPresent()) {
Mine m = mine.get(); Mine m = mine.get();
if (m.appliesToBlock(event.getBlock().getLocation())) { if (m.appliesToBlock(event.getBlock().getLocation())) {

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;
@ -14,13 +14,13 @@ import org.bukkit.inventory.InventoryView;
* Handles clicks in the group-create chest GUI. * Handles clicks in the group-create chest GUI.
*/ */
public final class GroupGuiListener implements Listener { public final class GroupGuiListener implements Listener {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
public GroupGuiListener(LoMinesPlugin plugin) { public GroupGuiListener(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
@EventHandler @EventHandler
public void onInventoryClick(InventoryClickEvent event) { public void onInventoryClick(InventoryClickEvent event) {
InventoryView view = event.getView(); InventoryView view = event.getView();
@ -41,7 +41,7 @@ public final class GroupGuiListener implements Listener {
} }
GroupCreateGui.handleClick(plugin, player, raw); GroupCreateGui.handleClick(plugin, player, raw);
} }
@EventHandler @EventHandler
public void onInventoryDrag(InventoryDragEvent event) { public void onInventoryDrag(InventoryDragEvent event) {
if (event.getInventory().getHolder() instanceof GroupCreateGuiHolder) { if (event.getInventory().getHolder() instanceof GroupCreateGuiHolder) {

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;
@ -19,13 +19,13 @@ import org.bukkit.inventory.EquipmentSlot;
* Group wand: ЛКМ / ПКМ по блоку — углы; Shift+ПКМ — GUI группы. * Group wand: ЛКМ / ПКМ по блоку — углы; Shift+ПКМ — GUI группы.
*/ */
public final class PlayerInteractListener implements Listener { public final class PlayerInteractListener implements Listener {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
public PlayerInteractListener(LoMinesPlugin plugin) { public PlayerInteractListener(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false) @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false)
public void onPlayerInteract(PlayerInteractEvent event) { public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND) { if (event.getHand() != EquipmentSlot.HAND) {
@ -36,13 +36,13 @@ public final class PlayerInteractListener implements Listener {
return; return;
} }
Action action = event.getAction(); Action action = event.getAction();
if (player.isSneaking() && (action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK)) { if (player.isSneaking() && (action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK)) {
event.setCancelled(true); event.setCancelled(true);
GroupCreateGui.open(plugin, player); GroupCreateGui.open(plugin, player);
return; return;
} }
if (action == Action.LEFT_CLICK_BLOCK && event.hasBlock()) { if (action == Action.LEFT_CLICK_BLOCK && event.hasBlock()) {
event.setCancelled(true); event.setCancelled(true);
Block block = event.getClickedBlock(); Block block = event.getClickedBlock();
@ -53,10 +53,10 @@ 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;
} }
if (action == Action.RIGHT_CLICK_BLOCK && !player.isSneaking()) { if (action == Action.RIGHT_CLICK_BLOCK && !player.isSneaking()) {
event.setCancelled(true); event.setCancelled(true);
Block block = event.getClickedBlock(); Block block = event.getClickedBlock();
@ -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;
@ -10,13 +10,13 @@ import org.bukkit.event.player.PlayerJoinEvent;
* This is a placeholder implementation that will be completed in task 11.3. * This is a placeholder implementation that will be completed in task 11.3.
*/ */
public final class PlayerJoinListener implements Listener { public final class PlayerJoinListener implements Listener {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
public PlayerJoinListener(LoMinesPlugin plugin) { public PlayerJoinListener(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
} }
@EventHandler @EventHandler
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
// Load or create player statistics // Load or create player statistics

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;
@ -10,23 +11,23 @@ import java.util.Set;
/** /**
* Utility class for updating chunks after block modifications. * Utility class for updating chunks after block modifications.
* Automatically detects Paper API availability and uses the most efficient method. * Automatically detects Paper API availability and uses the most efficient method.
* *
* <p>Paper detection is done through method reflection rather than Class.forName * <p>Paper detection is done through method reflection rather than Class.forName
* to provide a more reliable mechanism as per requirement 21.4.</p> * to provide a more reliable mechanism as per requirement 21.4.</p>
*/ */
public final class ChunkUtils { public final class ChunkUtils {
private static final boolean IS_PAPER = detectPaper(); private static final boolean IS_PAPER = detectPaper();
private static final Method SEND_CHUNK_CHANGE_METHOD = findSendChunkChangeMethod(); private static final Method SEND_CHUNK_CHANGE_METHOD = findSendChunkChangeMethod();
private ChunkUtils() { private ChunkUtils() {
throw new UnsupportedOperationException("Utility class"); throw new UnsupportedOperationException("Utility class");
} }
/** /**
* Updates all chunks within the given cuboid region. * Updates all chunks within the given cuboid region.
* Uses Paper API if available for better performance, otherwise falls back to Bukkit API. * Uses Paper API if available for better performance, otherwise falls back to Bukkit API.
* *
* @param region The cuboid region whose chunks should be updated * @param region The cuboid region whose chunks should be updated
* @throws IllegalArgumentException if region is null * @throws IllegalArgumentException if region is null
*/ */
@ -34,20 +35,20 @@ public final class ChunkUtils {
if (region == null) { if (region == null) {
throw new IllegalArgumentException("Region cannot be null"); throw new IllegalArgumentException("Region cannot be null");
} }
Set<ChunkCoordinate> chunks = getAffectedChunks(region); Set<ChunkCoordinate> chunks = getAffectedChunks(region);
World world = region.getWorld(); World world = region.getWorld();
for (ChunkCoordinate coord : chunks) { for (ChunkCoordinate coord : chunks) {
Chunk chunk = world.getChunkAt(coord.x, coord.z); Chunk chunk = world.getChunkAt(coord.x, coord.z);
updateChunk(chunk); updateChunk(chunk);
} }
} }
/** /**
* Detects if Paper API is available by checking for Paper-specific methods. * Detects if Paper API is available by checking for Paper-specific methods.
* This is more reliable than Class.forName as it checks for actual functionality. * This is more reliable than Class.forName as it checks for actual functionality.
* *
* @return true if Paper API is available, false otherwise * @return true if Paper API is available, false otherwise
*/ */
private static boolean detectPaper() { private static boolean detectPaper() {
@ -61,17 +62,17 @@ public final class ChunkUtils {
return false; return false;
} }
} }
/** /**
* Finds the sendChunkChange method if running on Paper. * Finds the sendChunkChange method if running on Paper.
* *
* @return The sendChunkChange method, or null if not available * @return The sendChunkChange method, or null if not available
*/ */
private static Method findSendChunkChangeMethod() { private static Method findSendChunkChangeMethod() {
if (!IS_PAPER) { if (!IS_PAPER) {
return null; return null;
} }
try { try {
Class<?> chunkClass = Chunk.class; Class<?> chunkClass = Chunk.class;
// Paper 1.21 has sendChunkChange() method // Paper 1.21 has sendChunkChange() method
@ -80,10 +81,10 @@ public final class ChunkUtils {
return null; return null;
} }
} }
/** /**
* Updates a single chunk using the appropriate API. * Updates a single chunk using the appropriate API.
* *
* @param chunk The chunk to update * @param chunk The chunk to update
*/ */
private static void updateChunk(Chunk chunk) { private static void updateChunk(Chunk chunk) {
@ -93,10 +94,10 @@ public final class ChunkUtils {
updateChunkBukkit(chunk); updateChunkBukkit(chunk);
} }
} }
/** /**
* Updates a chunk using Paper API. * Updates a chunk using Paper API.
* *
* @param chunk The chunk to update * @param chunk The chunk to update
*/ */
private static void updateChunkPaper(Chunk chunk) { private static void updateChunkPaper(Chunk chunk) {
@ -107,11 +108,11 @@ public final class ChunkUtils {
updateChunkBukkit(chunk); updateChunkBukkit(chunk);
} }
} }
/** /**
* Updates a chunk using Bukkit API. * Updates a chunk using Bukkit API.
* Forces chunk to be resent to all players by marking it as modified. * Forces chunk to be resent to all players by marking it as modified.
* *
* @param chunk The chunk to update * @param chunk The chunk to update
*/ */
private static void updateChunkBukkit(Chunk chunk) { private static void updateChunkBukkit(Chunk chunk) {
@ -120,7 +121,7 @@ public final class ChunkUtils {
// We can force a refresh by unloading and reloading, but that's expensive // We can force a refresh by unloading and reloading, but that's expensive
// Instead, we rely on the natural chunk update mechanism // Instead, we rely on the natural chunk update mechanism
// Players will see the changes when the chunk is next sent to them // Players will see the changes when the chunk is next sent to them
// For Bukkit, we can use the World's refreshChunk method if available // For Bukkit, we can use the World's refreshChunk method if available
World world = chunk.getWorld(); World world = chunk.getWorld();
try { try {
@ -132,54 +133,42 @@ public final class ChunkUtils {
// This is acceptable as blocks are already set // This is acceptable as blocks are already set
} }
} }
/** /**
* Calculates all chunk coordinates that are affected by the given region. * Calculates all chunk coordinates that are affected by the given region.
* *
* @param region The cuboid region * @param region The cuboid region
* @return Set of chunk coordinates * @return Set of chunk coordinates
*/ */
private static Set<ChunkCoordinate> getAffectedChunks(Cuboid region) { private static Set<ChunkCoordinate> getAffectedChunks(Cuboid region) {
Set<ChunkCoordinate> chunks = new HashSet<>(); Set<ChunkCoordinate> chunks = new HashSet<>();
// Convert block coordinates to chunk coordinates // Convert block coordinates to chunk coordinates
int minChunkX = region.getMinX() >> 4; int minChunkX = region.getMinX() >> 4;
int maxChunkX = region.getMaxX() >> 4; int maxChunkX = region.getMaxX() >> 4;
int minChunkZ = region.getMinZ() >> 4; int minChunkZ = region.getMinZ() >> 4;
int maxChunkZ = region.getMaxZ() >> 4; int maxChunkZ = region.getMaxZ() >> 4;
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
chunks.add(new ChunkCoordinate(chunkX, chunkZ)); chunks.add(new ChunkCoordinate(chunkX, chunkZ));
} }
} }
return chunks; return chunks;
} }
/** /**
* 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,23 +1,23 @@
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.
* Minecraft runs at 20 ticks per second. * Minecraft runs at 20 ticks per second.
*/ */
public final class TimeFormatter { public final class TimeFormatter {
private static final int TICKS_PER_SECOND = 20; private static final int TICKS_PER_SECOND = 20;
private static final int SECONDS_PER_MINUTE = 60; private static final int SECONDS_PER_MINUTE = 60;
private static final int MINUTES_PER_HOUR = 60; private static final int MINUTES_PER_HOUR = 60;
private TimeFormatter() { private TimeFormatter() {
throw new UnsupportedOperationException("Utility class"); throw new UnsupportedOperationException("Utility class");
} }
/** /**
* 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
@ -26,13 +26,13 @@ public final class TimeFormatter {
if (format == null || format.trim().isEmpty()) { if (format == null || format.trim().isEmpty()) {
throw new IllegalArgumentException("Format cannot be null or empty"); throw new IllegalArgumentException("Format cannot be null or empty");
} }
if (ticks < 0) { if (ticks < 0) {
ticks = 0; ticks = 0;
} }
int totalSeconds = ticks / TICKS_PER_SECOND; int totalSeconds = ticks / TICKS_PER_SECOND;
return switch (format.toLowerCase()) { return switch (format.toLowerCase()) {
case "mm:ss" -> formatMinutesSeconds(totalSeconds); case "mm:ss" -> formatMinutesSeconds(totalSeconds);
case "hh:mm:ss" -> formatHoursMinutesSeconds(totalSeconds); case "hh:mm:ss" -> formatHoursMinutesSeconds(totalSeconds);
@ -40,12 +40,12 @@ 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"
); );
}; };
} }
/** /**
* Formats time as "mm:ss" (minutes:seconds). * Formats time as "mm:ss" (minutes:seconds).
*/ */
@ -54,7 +54,7 @@ public final class TimeFormatter {
int seconds = totalSeconds % SECONDS_PER_MINUTE; int seconds = totalSeconds % SECONDS_PER_MINUTE;
return String.format("%02d:%02d", minutes, seconds); return String.format("%02d:%02d", minutes, seconds);
} }
/** /**
* Formats time as "hh:mm:ss" (hours:minutes:seconds). * Formats time as "hh:mm:ss" (hours:minutes:seconds).
*/ */

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;
@ -7,14 +7,14 @@ import org.bukkit.block.Block;
* Stable string keys for block-aligned locations (world + block integer coords). * Stable string keys for block-aligned locations (world + block integer coords).
*/ */
public final class BlockKeys { public final class BlockKeys {
private BlockKeys() { private BlockKeys() {
} }
public static String key(Block block) { public static String key(Block block) {
return key(block.getLocation()); return key(block.getLocation());
} }
public static String key(Location loc) { public static String key(Location loc) {
return loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ(); return loc.getWorld().getName() + ":" + loc.getBlockX() + ":" + loc.getBlockY() + ":" + loc.getBlockZ();
} }

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;
@ -6,11 +6,11 @@ import org.bukkit.World;
/** /**
* Immutable representation of a rectangular 3D region (cuboid) in a Minecraft world. * Immutable representation of a rectangular 3D region (cuboid) in a Minecraft world.
* Used to define mine boundaries and check block containment. * Used to define mine boundaries and check block containment.
* *
* <p>This class is thread-safe due to its immutability.</p> * <p>This class is thread-safe due to its immutability.</p>
*/ */
public final class Cuboid { public final class Cuboid {
private final World world; private final World world;
private final int minX; private final int minX;
private final int minY; private final int minY;
@ -18,11 +18,11 @@ public final class Cuboid {
private final int maxX; private final int maxX;
private final int maxY; private final int maxY;
private final int maxZ; private final int maxZ;
/** /**
* Creates a cuboid from two corner locations. * Creates a cuboid from two corner locations.
* The locations must be in the same world. * The locations must be in the same world.
* *
* @param loc1 First corner location * @param loc1 First corner location
* @param loc2 Second corner location (opposite corner) * @param loc2 Second corner location (opposite corner)
* @throws IllegalArgumentException if locations are in different worlds or null * @throws IllegalArgumentException if locations are in different worlds or null
@ -31,30 +31,30 @@ public final class Cuboid {
if (loc1 == null || loc2 == null) { if (loc1 == null || loc2 == null) {
throw new IllegalArgumentException("Locations cannot be null"); throw new IllegalArgumentException("Locations cannot be null");
} }
if (loc1.getWorld() == null || loc2.getWorld() == null) { if (loc1.getWorld() == null || loc2.getWorld() == null) {
throw new IllegalArgumentException("Location worlds cannot be null"); throw new IllegalArgumentException("Location worlds cannot be null");
} }
if (!loc1.getWorld().equals(loc2.getWorld())) { if (!loc1.getWorld().equals(loc2.getWorld())) {
throw new IllegalArgumentException("Locations must be in the same world"); throw new IllegalArgumentException("Locations must be in the same world");
} }
this.world = loc1.getWorld(); this.world = loc1.getWorld();
// Calculate min/max coordinates // Calculate min/max coordinates
this.minX = Math.min(loc1.getBlockX(), loc2.getBlockX()); this.minX = Math.min(loc1.getBlockX(), loc2.getBlockX());
this.minY = Math.min(loc1.getBlockY(), loc2.getBlockY()); this.minY = Math.min(loc1.getBlockY(), loc2.getBlockY());
this.minZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ()); this.minZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ());
this.maxX = Math.max(loc1.getBlockX(), loc2.getBlockX()); this.maxX = Math.max(loc1.getBlockX(), loc2.getBlockX());
this.maxY = Math.max(loc1.getBlockY(), loc2.getBlockY()); this.maxY = Math.max(loc1.getBlockY(), loc2.getBlockY());
this.maxZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ()); this.maxZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ());
} }
/** /**
* Checks if a location is contained within this cuboid. * Checks if a location is contained within this cuboid.
* *
* @param location The location to check * @param location The location to check
* @return true if the location is within this cuboid, false otherwise * @return true if the location is within this cuboid, false otherwise
*/ */
@ -62,23 +62,23 @@ public final class Cuboid {
if (location == null || location.getWorld() == null) { if (location == null || location.getWorld() == null) {
return false; return false;
} }
if (!location.getWorld().equals(world)) { if (!location.getWorld().equals(world)) {
return false; return false;
} }
int x = location.getBlockX(); int x = location.getBlockX();
int y = location.getBlockY(); int y = location.getBlockY();
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;
} }
/** /**
* Calculates the volume of this cuboid in blocks. * Calculates the volume of this cuboid in blocks.
* *
* @return The number of blocks contained in this cuboid * @return The number of blocks contained in this cuboid
*/ */
public int getVolume() { public int getVolume() {
@ -87,87 +87,86 @@ public final class Cuboid {
int depth = maxZ - minZ + 1; int depth = maxZ - minZ + 1;
return width * height * depth; return width * height * depth;
} }
/** /**
* Gets the world this cuboid is in. * Gets the world this cuboid is in.
* *
* @return The world * @return The world
*/ */
public World getWorld() { public World getWorld() {
return world; return world;
} }
/** /**
* Gets the minimum X coordinate. * Gets the minimum X coordinate.
* *
* @return Minimum X * @return Minimum X
*/ */
public int getMinX() { public int getMinX() {
return minX; return minX;
} }
/** /**
* Gets the minimum Y coordinate. * Gets the minimum Y coordinate.
* *
* @return Minimum Y * @return Minimum Y
*/ */
public int getMinY() { public int getMinY() {
return minY; return minY;
} }
/** /**
* Gets the minimum Z coordinate. * Gets the minimum Z coordinate.
* *
* @return Minimum Z * @return Minimum Z
*/ */
public int getMinZ() { public int getMinZ() {
return minZ; return minZ;
} }
/** /**
* Gets the maximum X coordinate. * Gets the maximum X coordinate.
* *
* @return Maximum X * @return Maximum X
*/ */
public int getMaxX() { public int getMaxX() {
return maxX; return maxX;
} }
/** /**
* Gets the maximum Y coordinate. * Gets the maximum Y coordinate.
* *
* @return Maximum Y * @return Maximum Y
*/ */
public int getMaxY() { public int getMaxY() {
return maxY; return maxY;
} }
/** /**
* Gets the maximum Z coordinate. * Gets the maximum Z coordinate.
* *
* @return Maximum Z * @return Maximum Z
*/ */
public int getMaxZ() { public int getMaxZ() {
return maxZ; return maxZ;
} }
@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
public int hashCode() { public int hashCode() {
int result = world.hashCode(); int result = world.hashCode();

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;
@ -14,14 +14,14 @@ import java.util.regex.Pattern;
* Format: "world;x;y;z;yaw;pitch" * Format: "world;x;y;z;yaw;pitch"
*/ */
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");
} }
/** /**
* Parses a location string in the format "world;x;y;z;yaw;pitch". * Parses a location string in the format "world;x;y;z;yaw;pitch".
* *
@ -33,15 +33,15 @@ public final class LocationParser {
if (locationString == null || locationString.trim().isEmpty()) { if (locationString == null || locationString.trim().isEmpty()) {
throw new ConfigParseException("Location string cannot be null or empty"); throw new ConfigParseException("Location string cannot be null or empty");
} }
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'"
); );
} }
try { try {
String worldName = matcher.group(1); String worldName = matcher.group(1);
double x = Double.parseDouble(matcher.group(2)); double x = Double.parseDouble(matcher.group(2));
@ -49,24 +49,24 @@ public final class LocationParser {
double z = Double.parseDouble(matcher.group(4)); double z = Double.parseDouble(matcher.group(4));
float yaw = Float.parseFloat(matcher.group(5)); float yaw = Float.parseFloat(matcher.group(5));
float pitch = Float.parseFloat(matcher.group(6)); float pitch = Float.parseFloat(matcher.group(6));
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"
); );
} }
return new Location(world, x, y, z, yaw, pitch); return new Location(world, x, y, z, yaw, pitch);
} 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
); );
} }
} }
/** /**
* Formats a Location object to a string in the format "world;x;y;z;yaw;pitch". * Formats a Location object to a string in the format "world;x;y;z;yaw;pitch".
* *
@ -78,19 +78,19 @@ public final class LocationParser {
if (location == null) { if (location == null) {
throw new IllegalArgumentException("Location cannot be null"); throw new IllegalArgumentException("Location cannot be null");
} }
World world = location.getWorld(); World world = location.getWorld();
if (world == null) { if (world == null) {
throw new IllegalArgumentException("Location world cannot be null"); throw new IllegalArgumentException("Location world cannot be null");
} }
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;
@ -12,10 +13,10 @@ import java.util.List;
* Finds all blocks of a given vanilla material inside cuboid regions (marker paint for mask mode). * Finds all blocks of a given vanilla material inside cuboid regions (marker paint for mask mode).
*/ */
public final class MaskScanner { public final class MaskScanner {
private MaskScanner() { private MaskScanner() {
} }
/** /**
* Collects block centers for every block whose type equals {@code marker} inside {@code regions}. * Collects block centers for every block whose type equals {@code marker} inside {@code regions}.
*/ */

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;
@ -10,27 +11,27 @@ import java.util.Optional;
* Manages selection points for defining mine regions. * Manages selection points for defining mine regions.
* Stores up to 10 points (5 pairs) for multi-regional mines. * Stores up to 10 points (5 pairs) for multi-regional mines.
* Points are organized in pairs where indices 2i and 2i+1 form a pair for i ∈ [0, 4]. * Points are organized in pairs where indices 2i and 2i+1 form a pair for i ∈ [0, 4].
* *
* <p>This class is NOT thread-safe and should be used per-player.</p> * <p>This class is NOT thread-safe and should be used per-player.</p>
*/ */
public final class Selection { public final class Selection {
private static final int MAX_POINTS = 10; private static final int MAX_POINTS = 10;
private static final int MAX_PAIRS = 5; private static final int MAX_PAIRS = 5;
private final Location[] points; private final Location[] points;
/** /**
* Creates a new empty selection. * Creates a new empty selection.
*/ */
public Selection() { public Selection() {
this.points = new Location[MAX_POINTS]; this.points = new Location[MAX_POINTS];
} }
/** /**
* 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
*/ */
@ -38,10 +39,10 @@ public final class Selection {
validateIndex(index); validateIndex(index);
points[index] = location; points[index] = location;
} }
/** /**
* Gets a point at the specified index. * Gets a point at the specified index.
* *
* @param index The point index (0-9) * @param index The point index (0-9)
* @return Optional containing the location if set, empty otherwise * @return Optional containing the location if set, empty otherwise
* @throws IllegalArgumentException if index is out of bounds * @throws IllegalArgumentException if index is out of bounds
@ -50,11 +51,11 @@ public final class Selection {
validateIndex(index); validateIndex(index);
return Optional.ofNullable(points[index]); return Optional.ofNullable(points[index]);
} }
/** /**
* Gets a pair of points as an array. * Gets a pair of points as an array.
* A pair consists of points at indices 2*pairIndex and 2*pairIndex+1. * A pair consists of points at indices 2*pairIndex and 2*pairIndex+1.
* *
* @param pairIndex The pair index (0-4) * @param pairIndex The pair index (0-4)
* @return Array of two locations [point1, point2], may contain nulls * @return Array of two locations [point1, point2], may contain nulls
* @throws IllegalArgumentException if pairIndex is out of bounds * @throws IllegalArgumentException if pairIndex is out of bounds
@ -63,12 +64,12 @@ 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]};
} }
/** /**
* Checks if a pair has both points set. * Checks if a pair has both points set.
* *
* @param pairIndex The pair index (0-4) * @param pairIndex The pair index (0-4)
* @return true if both points in the pair are set, false otherwise * @return true if both points in the pair are set, false otherwise
* @throws IllegalArgumentException if pairIndex is out of bounds * @throws IllegalArgumentException if pairIndex is out of bounds
@ -79,7 +80,7 @@ public final class Selection {
int secondIndex = firstIndex + 1; int secondIndex = firstIndex + 1;
return points[firstIndex] != null && points[secondIndex] != null; return points[firstIndex] != null && points[secondIndex] != null;
} }
/** /**
* Clears all selection points. * Clears all selection points.
*/ */
@ -88,50 +89,50 @@ public final class Selection {
points[i] = null; points[i] = null;
} }
} }
/** /**
* Converts all complete pairs to Cuboid regions. * Converts all complete pairs to Cuboid regions.
* Only pairs with both points set are converted. * Only pairs with both points set are converted.
* *
* @return List of Cuboid regions created from complete pairs * @return List of Cuboid regions created from complete pairs
*/ */
public List<Cuboid> toCuboids() { public List<Cuboid> toCuboids() {
List<Cuboid> cuboids = new ArrayList<>(); List<Cuboid> cuboids = new ArrayList<>();
for (int pairIndex = 0; pairIndex < MAX_PAIRS; pairIndex++) { for (int pairIndex = 0; pairIndex < MAX_PAIRS; pairIndex++) {
if (hasPair(pairIndex)) { if (hasPair(pairIndex)) {
Location[] pair = getPair(pairIndex); Location[] pair = getPair(pairIndex);
cuboids.add(new Cuboid(pair[0], pair[1])); cuboids.add(new Cuboid(pair[0], pair[1]));
} }
} }
return cuboids; return cuboids;
} }
/** /**
* Validates that an index is within valid bounds [0, 9]. * Validates that an index is within valid bounds [0, 9].
* *
* @param index The index to validate * @param index The index to validate
* @throws IllegalArgumentException if index is out of bounds * @throws IllegalArgumentException if index is out of bounds
*/ */
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
); );
} }
} }
/** /**
* Validates that a pair index is within valid bounds [0, 4]. * Validates that a pair index is within valid bounds [0, 4].
* *
* @param pairIndex The pair index to validate * @param pairIndex The pair index to validate
* @throws IllegalArgumentException if pairIndex is out of bounds * @throws IllegalArgumentException if pairIndex is out of bounds
*/ */
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;
@ -9,25 +9,25 @@ import java.util.concurrent.ConcurrentHashMap;
* Manages player selections for mine region definition. * Manages player selections for mine region definition.
* Stores a Selection object for each player, allowing multiple players * Stores a Selection object for each player, allowing multiple players
* to have independent selections simultaneously. * to have independent selections simultaneously.
* *
* <p>This class is thread-safe and can be accessed from multiple threads.</p> * <p>This class is thread-safe and can be accessed from multiple threads.</p>
* *
* <p><b>Validates:</b> Requirements 12.7, 16.7</p> * <p><b>Validates:</b> Requirements 12.7, 16.7</p>
*/ */
public final class SelectionManager { public final class SelectionManager {
private final Map<UUID, Selection> selections; private final Map<UUID, Selection> selections;
/** /**
* Creates a new SelectionManager with thread-safe storage. * Creates a new SelectionManager with thread-safe storage.
*/ */
public SelectionManager() { public SelectionManager() {
this.selections = new ConcurrentHashMap<>(); this.selections = new ConcurrentHashMap<>();
} }
/** /**
* Gets the selection for a player, creating a new one if it doesn't exist. * Gets the selection for a player, creating a new one if it doesn't exist.
* *
* @param playerId The player's UUID * @param playerId The player's UUID
* @return The player's Selection object * @return The player's Selection object
* @throws IllegalArgumentException if playerId is null * @throws IllegalArgumentException if playerId is null
@ -36,10 +36,10 @@ public final class SelectionManager {
validatePlayerId(playerId); validatePlayerId(playerId);
return selections.computeIfAbsent(playerId, id -> new Selection()); return selections.computeIfAbsent(playerId, id -> new Selection());
} }
/** /**
* Gets the selection for a player if it exists. * Gets the selection for a player if it exists.
* *
* @param playerId The player's UUID * @param playerId The player's UUID
* @return Optional containing the Selection if it exists, empty otherwise * @return Optional containing the Selection if it exists, empty otherwise
* @throws IllegalArgumentException if playerId is null * @throws IllegalArgumentException if playerId is null
@ -48,10 +48,10 @@ public final class SelectionManager {
validatePlayerId(playerId); validatePlayerId(playerId);
return Optional.ofNullable(selections.get(playerId)); return Optional.ofNullable(selections.get(playerId));
} }
/** /**
* Checks if a player has a selection. * Checks if a player has a selection.
* *
* @param playerId The player's UUID * @param playerId The player's UUID
* @return true if the player has a selection, false otherwise * @return true if the player has a selection, false otherwise
* @throws IllegalArgumentException if playerId is null * @throws IllegalArgumentException if playerId is null
@ -60,10 +60,10 @@ public final class SelectionManager {
validatePlayerId(playerId); validatePlayerId(playerId);
return selections.containsKey(playerId); return selections.containsKey(playerId);
} }
/** /**
* Clears the selection for a player, removing it from storage. * Clears the selection for a player, removing it from storage.
* *
* @param playerId The player's UUID * @param playerId The player's UUID
* @throws IllegalArgumentException if playerId is null * @throws IllegalArgumentException if playerId is null
*/ */
@ -71,10 +71,10 @@ public final class SelectionManager {
validatePlayerId(playerId); validatePlayerId(playerId);
selections.remove(playerId); selections.remove(playerId);
} }
/** /**
* Validates that a player ID is not null. * Validates that a player ID is not null.
* *
* @param playerId The player ID to validate * @param playerId The player ID to validate
* @throws IllegalArgumentException if playerId is null * @throws IllegalArgumentException if playerId is null
*/ */

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;
@ -15,37 +15,39 @@ import java.util.List;
* Blaze rod marked with PDC so only this item drives group wand behaviour. * Blaze rod marked with PDC so only this item drives group wand behaviour.
*/ */
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() {
} }
public static NamespacedKey key(LoMinesPlugin plugin) { public static NamespacedKey key(LoMinesPlugin plugin) {
return new NamespacedKey(NAMESPACE, KEY); return new NamespacedKey(NAMESPACE, KEY);
} }
public static ItemStack create(LoMinesPlugin plugin) { public static ItemStack create(LoMinesPlugin plugin) {
ItemStack item = new ItemStack(Material.BLAZE_ROD); ItemStack item = new ItemStack(Material.BLAZE_ROD);
ItemMeta meta = item.getItemMeta(); ItemMeta meta = item.getItemMeta();
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);
} }
return item; return item;
} }
public static boolean isWand(LoMinesPlugin plugin, ItemStack stack) { public static boolean isWand(LoMinesPlugin plugin, ItemStack stack) {
if (stack == null || stack.getType() == Material.AIR) { if (stack == null || stack.getType() == Material.AIR) {
return false; return false;

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;
@ -8,13 +8,13 @@ import java.util.concurrent.ConcurrentHashMap;
* Per-player {@link GroupWandSession} for the group wand workflow. * Per-player {@link GroupWandSession} for the group wand workflow.
*/ */
public final class GroupWandManager { public final class GroupWandManager {
private final Map<UUID, GroupWandSession> sessions = new ConcurrentHashMap<>(); private final Map<UUID, GroupWandSession> sessions = new ConcurrentHashMap<>();
public GroupWandSession getSession(UUID playerId) { public GroupWandSession getSession(UUID playerId) {
return sessions.computeIfAbsent(playerId, u -> new GroupWandSession()); return sessions.computeIfAbsent(playerId, u -> new GroupWandSession());
} }
public void removeSession(UUID playerId) { public void removeSession(UUID playerId) {
sessions.remove(playerId); sessions.remove(playerId);
} }

View file

@ -1,4 +1,4 @@
package com.loki.lomines.wand; package dev.loki.lomines.wand;
import org.bukkit.Location; import org.bukkit.Location;
@ -9,67 +9,66 @@ import java.util.Arrays;
* each slot has its own cuboid (pos1 + pos2). * each slot has its own cuboid (pos1 + pos2).
*/ */
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;
} }
public void setBaseName(String baseName) { public void setBaseName(String baseName) {
this.baseName = baseName != null ? baseName.trim() : ""; this.baseName = baseName != null ? baseName.trim() : "";
} }
public boolean hasBaseName() { public boolean hasBaseName() {
return baseName != null && !baseName.isEmpty(); return baseName != null && !baseName.isEmpty();
} }
public int getActiveSlot() { public int getActiveSlot() {
return activeSlot; return activeSlot;
} }
public void setActiveSlot(int activeSlot) { public void setActiveSlot(int activeSlot) {
if (activeSlot < 0 || activeSlot >= SLOTS) { if (activeSlot < 0 || activeSlot >= SLOTS) {
throw new IllegalArgumentException("activeSlot 0..8"); throw new IllegalArgumentException("activeSlot 0..8");
} }
this.activeSlot = activeSlot; this.activeSlot = activeSlot;
} }
public Location getPos1(int index) { public Location getPos1(int index) {
return pos1[index]; return pos1[index];
} }
public Location getPos2(int index) { public Location getPos2(int index) {
return pos2[index]; return pos2[index];
} }
public void setPos1(int index, Location loc) { public void setPos1(int index, Location loc) {
pos1[index] = loc; pos1[index] = loc;
} }
public void setPos2(int index, Location loc) { public void setPos2(int index, Location loc) {
pos2[index] = loc; pos2[index] = loc;
} }
public boolean isSlotReady(int index) { public boolean isSlotReady(int index) {
return pos1[index] != null && pos2[index] != null; return pos1[index] != null && pos2[index] != null;
} }
public void clearCorners(int index) { public void clearCorners(int index) {
pos1[index] = null; pos1[index] = null;
pos2[index] = null; pos2[index] = null;
} }
public void clearAll() { public void clearAll() {
Arrays.fill(pos1, null); Arrays.fill(pos1, null);
Arrays.fill(pos2, null); Arrays.fill(pos2, null);
} }
public int countReadySlots() { public int countReadySlots() {
int n = 0; int n = 0;
for (int i = 0; i < SLOTS; i++) { for (int i = 0; i < SLOTS; i++) {

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;
@ -23,54 +23,52 @@ import static org.mockito.Mockito.when;
* Unit tests for Mine class, specifically testing BlockSetter creation logic. * Unit tests for Mine class, specifically testing BlockSetter creation logic.
*/ */
class MineTest { class MineTest {
@Mock @Mock
private LoMinesPlugin plugin; private LoMinesPlugin plugin;
@Mock @Mock
private World world; private World world;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
MockitoAnnotations.openMocks(this); MockitoAnnotations.openMocks(this);
} }
@Test @Test
void testCreateBlockSetter_WithOraxenPrefix_CreatesOraxenBlockSetter() { void testCreateBlockSetter_WithOraxenPrefix_CreatesOraxenBlockSetter() {
// Arrange // Arrange
Map<String, Double> blocks = new HashMap<>(); Map<String, Double> blocks = new HashMap<>();
blocks.put("oraxen:custom_ore", 50.0); blocks.put("oraxen:custom_ore", 50.0);
blocks.put("oraxen:rare_ore", 30.0); blocks.put("oraxen:rare_ore", 30.0);
MineConfig config = createTestConfig(blocks); MineConfig config = createTestConfig(blocks);
// Act // Act
Mine mine = new Mine("test_mine", config, plugin); Mine mine = new Mine("test_mine", config, plugin);
// 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
void testCreateBlockSetter_WithItemsAdderPrefix_CreatesItemsAdderBlockSetter() { void testCreateBlockSetter_WithItemsAdderPrefix_CreatesItemsAdderBlockSetter() {
// Arrange // Arrange
Map<String, Double> blocks = new HashMap<>(); Map<String, Double> blocks = new HashMap<>();
blocks.put("itemsadder:custom_block", 60.0); blocks.put("itemsadder:custom_block", 60.0);
blocks.put("itemsadder:special_block", 40.0); blocks.put("itemsadder:special_block", 40.0);
MineConfig config = createTestConfig(blocks); MineConfig config = createTestConfig(blocks);
// Act // Act
Mine mine = new Mine("test_mine", config, plugin); Mine mine = new Mine("test_mine", config, plugin);
// 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
void testCreateBlockSetter_WithoutPrefix_CreatesBukkitBlockSetter() { void testCreateBlockSetter_WithoutPrefix_CreatesBukkitBlockSetter() {
// Arrange // Arrange
@ -78,39 +76,38 @@ class MineTest {
blocks.put("STONE", 50.0); blocks.put("STONE", 50.0);
blocks.put("COAL_ORE", 30.0); blocks.put("COAL_ORE", 30.0);
blocks.put("IRON_ORE", 20.0); blocks.put("IRON_ORE", 20.0);
MineConfig config = createTestConfig(blocks); MineConfig config = createTestConfig(blocks);
// Act // Act
Mine mine = new Mine("test_mine", config, plugin); Mine mine = new Mine("test_mine", config, plugin);
// 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
void testCreateBlockSetter_WithEmptyBlocks_ThrowsException() { void testCreateBlockSetter_WithEmptyBlocks_ThrowsException() {
// Arrange // Arrange
Map<String, Double> blocks = new HashMap<>(); Map<String, Double> blocks = new HashMap<>();
MineConfig config = createTestConfig(blocks); MineConfig config = createTestConfig(blocks);
// Act & Assert // Act & Assert
IllegalArgumentException exception = assertThrows( IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> new Mine("test_mine", config, plugin) () -> new Mine("test_mine", config, plugin)
); );
assertEquals("Mine must have at least one block type", exception.getMessage()); assertEquals("Mine must have at least one block type", exception.getMessage());
} }
private MineConfig createTestConfig(Map<String, Double> blocks) { private MineConfig createTestConfig(Map<String, Double> blocks) {
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
Location loc1 = new Location(world, 0, 0, 0); Location loc1 = new Location(world, 0, 0, 0);
Location loc2 = new Location(world, 10, 10, 10); Location loc2 = new Location(world, 10, 10, 10);
return MineConfig.builder() return MineConfig.builder()
.selections(List.of(loc1, loc2)) .selections(List.of(loc1, loc2))
.blocks(blocks) .blocks(blocks)

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.
@ -14,38 +14,38 @@ import static org.junit.jupiter.api.Assertions.*;
* These tests verify basic construction and null safety. * These tests verify basic construction and null safety.
*/ */
class MineTickerTest { class MineTickerTest {
@Mock @Mock
private LoMinesPlugin plugin; private LoMinesPlugin plugin;
@Mock @Mock
private Mines mines; private Mines mines;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
MockitoAnnotations.openMocks(this); MockitoAnnotations.openMocks(this);
} }
@Test @Test
void testConstruction_WithValidParameters_DoesNotThrow() { void testConstruction_WithValidParameters_DoesNotThrow() {
// Act & Assert // Act & Assert
assertDoesNotThrow(() -> new MineTicker(mines, plugin)); assertDoesNotThrow(() -> new MineTicker(mines, plugin));
} }
@Test @Test
void testStop_WhenNotRunning_DoesNotThrow() { void testStop_WhenNotRunning_DoesNotThrow() {
// Arrange // Arrange
MineTicker ticker = new MineTicker(mines, plugin); MineTicker ticker = new MineTicker(mines, plugin);
// Act & Assert // Act & Assert
assertDoesNotThrow(() -> ticker.stop()); assertDoesNotThrow(() -> ticker.stop());
} }
@Test @Test
void testMultipleStops_DoNotThrow() { void testMultipleStops_DoNotThrow() {
// Arrange // Arrange
MineTicker ticker = new MineTicker(mines, plugin); MineTicker ticker = new MineTicker(mines, plugin);
// Act & Assert // Act & Assert
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
ticker.stop(); ticker.stop();

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;
@ -28,28 +28,27 @@ import static org.mockito.Mockito.*;
* Tests loading and creating mines. * Tests loading and creating mines.
*/ */
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);
Logger logger = mock(Logger.class); Logger logger = mock(Logger.class);
LoLogger loLogger = mock(LoLogger.class); LoLogger loLogger = mock(LoLogger.class);
scheduler = mock(Scheduler.class); scheduler = mock(Scheduler.class);
when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); when(plugin.getDataFolder()).thenReturn(tempDir.toFile());
when(plugin.getLogger()).thenReturn(logger); when(plugin.getLogger()).thenReturn(logger);
when(plugin.loLogger()).thenReturn(loLogger); when(plugin.loLogger()).thenReturn(loLogger);
mines = new Mines(plugin); mines = new Mines(plugin);
} }
/** /**
* Helper method to set up Bukkit and Scheduler mocking for tests that create mines. * Helper method to set up Bukkit and Scheduler mocking for tests that create mines.
*/ */
@ -57,131 +56,131 @@ class MinesTest {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
BlockData blockData = mock(BlockData.class); BlockData blockData = mock(BlockData.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
bukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData); bukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler); schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
// Mock scheduler methods to execute callbacks immediately for testing // Mock scheduler methods to execute callbacks immediately for testing
ScheduledTask task = mock(ScheduledTask.class); ScheduledTask task = mock(ScheduledTask.class);
when(scheduler.runTimer(any(Runnable.class), anyLong(), anyLong())).thenReturn(task); when(scheduler.runTimer(any(Runnable.class), anyLong(), anyLong())).thenReturn(task);
} }
@Test @Test
void testLoadAllWithNoMinesFolder() throws IOException { void testLoadAllWithNoMinesFolder() throws IOException {
// When mines folder doesn't exist, loadAll should not throw // When mines folder doesn't exist, loadAll should not throw
assertDoesNotThrow(() -> mines.loadAll()); assertDoesNotThrow(() -> mines.loadAll());
// No mines should be loaded // No mines should be loaded
assertEquals(0, mines.getAll().size()); assertEquals(0, mines.getAll().size());
} }
@Test @Test
void testLoadAllWithEmptyMinesFolder() throws IOException { void testLoadAllWithEmptyMinesFolder() throws IOException {
// Create empty mines folder // Create empty mines folder
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
// Load all mines // Load all mines
mines.loadAll(); mines.loadAll();
// No mines should be loaded // No mines should be loaded
assertEquals(0, mines.getAll().size()); assertEquals(0, mines.getAll().size());
} }
@Test @Test
void testLoadAllWithValidMine() throws IOException { void testLoadAllWithValidMine() throws IOException {
// Create mines folder // Create mines folder
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
// 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);
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines // Load all mines
mines.loadAll(); mines.loadAll();
// Verify mine was loaded // Verify mine was loaded
assertEquals(1, mines.getAll().size()); assertEquals(1, mines.getAll().size());
assertTrue(mines.find("testmine").isPresent()); assertTrue(mines.find("testmine").isPresent());
Mine mine = mines.get("testmine"); Mine mine = mines.get("testmine");
assertNotNull(mine); assertNotNull(mine);
assertEquals("testmine", mine.getName()); assertEquals("testmine", mine.getName());
} }
} }
@Test @Test
void testLoadAllWithMultipleMines() throws IOException { void testLoadAllWithMultipleMines() throws IOException {
// Create mines folder // Create mines folder
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
// 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);
Files.writeString(minesFolder.resolve("mine3.yml"), yamlContent); Files.writeString(minesFolder.resolve("mine3.yml"), yamlContent);
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines // Load all mines
mines.loadAll(); mines.loadAll();
// Verify all mines were loaded // Verify all mines were loaded
assertEquals(3, mines.getAll().size()); assertEquals(3, mines.getAll().size());
assertTrue(mines.find("mine1").isPresent()); assertTrue(mines.find("mine1").isPresent());
@ -189,211 +188,211 @@ class MinesTest {
assertTrue(mines.find("mine3").isPresent()); assertTrue(mines.find("mine3").isPresent());
} }
} }
@Test @Test
void testLoadAllIgnoresNonYamlFiles() throws IOException { void testLoadAllIgnoresNonYamlFiles() throws IOException {
// Create mines folder // Create mines folder
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
// 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");
Files.writeString(minesFolder.resolve("backup.bak"), "Backup file"); Files.writeString(minesFolder.resolve("backup.bak"), "Backup file");
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines // Load all mines
mines.loadAll(); mines.loadAll();
// Only the .yml file should be loaded // Only the .yml file should be loaded
assertEquals(1, mines.getAll().size()); assertEquals(1, mines.getAll().size());
assertTrue(mines.find("validmine").isPresent()); assertTrue(mines.find("validmine").isPresent());
} }
} }
@Test @Test
void testLoadAllWithInvalidMine() throws IOException { void testLoadAllWithInvalidMine() throws IOException {
// Create mines folder // Create mines folder
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
// 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);
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines - should not throw, but should log error // Load all mines - should not throw, but should log error
assertDoesNotThrow(() -> mines.loadAll()); assertDoesNotThrow(() -> mines.loadAll());
// Invalid mine should not be loaded // Invalid mine should not be loaded
assertEquals(0, mines.getAll().size()); assertEquals(0, mines.getAll().size());
// Verify error was logged // Verify error was logged
verify(plugin.loLogger(), atLeastOnce()).error(anyString()); verify(plugin.loLogger(), atLeastOnce()).error(anyString());
} }
} }
@Test @Test
void testCreateNewMine() throws IOException { void testCreateNewMine() throws IOException {
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a new mine // Create a new mine
mines.create("newmine"); mines.create("newmine");
// Verify mine was created // Verify mine was created
assertEquals(1, mines.getAll().size()); assertEquals(1, mines.getAll().size());
assertTrue(mines.find("newmine").isPresent()); assertTrue(mines.find("newmine").isPresent());
Mine mine = mines.get("newmine"); Mine mine = mines.get("newmine");
assertNotNull(mine); assertNotNull(mine);
assertEquals("newmine", mine.getName()); assertEquals("newmine", mine.getName());
// Verify config file was created // Verify config file was created
Path configFile = tempDir.resolve("mines/newmine.yml"); Path configFile = tempDir.resolve("mines/newmine.yml");
assertTrue(Files.exists(configFile)); assertTrue(Files.exists(configFile));
// Verify logger was called // Verify logger was called
verify(plugin.loLogger()).info("Created mine: newmine"); verify(plugin.loLogger()).info("Created mine: newmine");
} }
} }
@Test @Test
void testCreateMineCreatesFolder() throws IOException { void testCreateMineCreatesFolder() throws IOException {
// Ensure mines folder doesn't exist // Ensure mines folder doesn't exist
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
assertFalse(Files.exists(minesFolder)); assertFalse(Files.exists(minesFolder));
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a new mine // Create a new mine
mines.create("testmine"); mines.create("testmine");
// Verify mines folder was created // Verify mines folder was created
assertTrue(Files.exists(minesFolder)); assertTrue(Files.exists(minesFolder));
assertTrue(Files.isDirectory(minesFolder)); assertTrue(Files.isDirectory(minesFolder));
} }
} }
@Test @Test
void testCreateDuplicateMineThrowsException() throws IOException { void testCreateDuplicateMineThrowsException() throws IOException {
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine // Create a mine
mines.create("duplicate"); mines.create("duplicate");
// Try to create the same mine again // Try to create the same mine again
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
mines.create("duplicate"); mines.create("duplicate");
}); });
assertTrue(exception.getMessage().contains("Mine already exists")); assertTrue(exception.getMessage().contains("Mine already exists"));
} }
} }
@Test @Test
void testCreateMineIsCaseInsensitive() throws IOException { void testCreateMineIsCaseInsensitive() throws IOException {
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine with mixed case // Create a mine with mixed case
mines.create("TestMine"); mines.create("TestMine");
// Try to create the same mine with different case // Try to create the same mine with different case
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
mines.create("testmine"); mines.create("testmine");
}); });
assertTrue(exception.getMessage().contains("Mine already exists")); assertTrue(exception.getMessage().contains("Mine already exists"));
} }
} }
@Test @Test
void testGetMineByName() throws IOException { void testGetMineByName() throws IOException {
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine // Create a mine
mines.create("testmine"); mines.create("testmine");
// Get mine by exact name // Get mine by exact name
Mine mine = mines.get("testmine"); Mine mine = mines.get("testmine");
assertNotNull(mine); assertNotNull(mine);
assertEquals("testmine", mine.getName()); assertEquals("testmine", mine.getName());
// Get mine by different case // Get mine by different case
Mine mine2 = mines.get("TestMine"); Mine mine2 = mines.get("TestMine");
assertNotNull(mine2); assertNotNull(mine2);
assertEquals("testmine", mine2.getName()); assertEquals("testmine", mine2.getName());
// Same mine instance // Same mine instance
assertSame(mine, mine2); assertSame(mine, mine2);
} }
} }
@Test @Test
void testGetNonExistentMineThrowsException() { void testGetNonExistentMineThrowsException() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
mines.get("nonexistent"); mines.get("nonexistent");
}); });
assertTrue(exception.getMessage().contains("Mine not found")); assertTrue(exception.getMessage().contains("Mine not found"));
} }
@Test @Test
void testFindMineReturnsOptional() throws IOException { void testFindMineReturnsOptional() throws IOException {
// Mock Bukkit and Scheduler // Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class); try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic); setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine // Create a mine
mines.create("testmine"); mines.create("testmine");
// Find existing mine // Find existing mine
assertTrue(mines.find("testmine").isPresent()); assertTrue(mines.find("testmine").isPresent());
// Find non-existent mine // Find non-existent mine
assertFalse(mines.find("nonexistent").isPresent()); assertFalse(mines.find("nonexistent").isPresent());
} }

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;
@ -25,72 +25,71 @@ import static org.mockito.Mockito.*;
* Tests parsing of YAML configuration files into MineConfig objects. * Tests parsing of YAML configuration files into MineConfig objects.
*/ */
class ConfigLoaderTest { class ConfigLoaderTest {
private ConfigLoader configLoader;
@TempDir @TempDir
Path tempDir; Path tempDir;
private ConfigLoader configLoader;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
configLoader = new ConfigLoader(); configLoader = new ConfigLoader();
} }
@Test @Test
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);
// Mock Bukkit // Mock Bukkit
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
// Load the config // Load the config
MineConfig config = configLoader.load(configFile); MineConfig config = configLoader.load(configFile);
// Verify selections // Verify selections
assertNotNull(config.getSelections()); assertNotNull(config.getSelections());
assertEquals(2, config.getSelections().size()); assertEquals(2, config.getSelections().size());
// Verify blocks // Verify blocks
assertNotNull(config.getBlocks()); assertNotNull(config.getBlocks());
assertEquals(3, config.getBlocks().size()); assertEquals(3, config.getBlocks().size());
@ -100,7 +99,7 @@ class ConfigLoaderTest {
assertEquals(50.0, config.getBlocks().get("stone")); assertEquals(50.0, config.getBlocks().get("stone"));
assertEquals(30.0, config.getBlocks().get("coal_ore")); assertEquals(30.0, config.getBlocks().get("coal_ore"));
assertEquals(20.0, config.getBlocks().get("iron_ore")); assertEquals(20.0, config.getBlocks().get("iron_ore"));
// Verify rewards // Verify rewards
assertNotNull(config.getRewards()); assertNotNull(config.getRewards());
assertEquals(1, config.getRewards().size()); assertEquals(1, config.getRewards().size());
@ -109,17 +108,17 @@ class ConfigLoaderTest {
assertFalse(reward.isPreventDrops()); assertFalse(reward.isPreventDrops());
assertEquals(1, reward.getMaterials().size()); assertEquals(1, reward.getMaterials().size());
assertTrue(reward.getMaterials().contains(Material.DIAMOND_ORE)); assertTrue(reward.getMaterials().contains(Material.DIAMOND_ORE));
// Verify reset settings // Verify reset settings
assertEquals(6000, config.getResetTicks()); assertEquals(6000, config.getResetTicks());
assertEquals(10.0, config.getResetPercent()); assertEquals(10.0, config.getResetPercent());
assertFalse(config.isResetOnPercentEnabled()); assertFalse(config.isResetOnPercentEnabled());
// Verify action bar settings // Verify action bar settings
assertTrue(config.isActionBarEnabled()); assertTrue(config.isActionBarEnabled());
assertEquals("Mine resetting soon", config.getActionBarMessage()); assertEquals("Mine resetting soon", config.getActionBarMessage());
assertEquals(50.0, config.getActionBarRange()); assertEquals(50.0, config.getActionBarRange());
// Verify other settings // Verify other settings
assertEquals("mm:ss", config.getTimerFormat()); assertEquals("mm:ss", config.getTimerFormat());
assertFalse(config.isTeleportOnReset()); assertFalse(config.isTeleportOnReset());
@ -129,68 +128,68 @@ class ConfigLoaderTest {
assertEquals("Mine has been reset", config.getBroadcastReset()); assertEquals("Mine has been reset", config.getBroadcastReset());
} }
} }
@Test @Test
void testLoadMissingFile() { void testLoadMissingFile() {
Path nonExistentFile = tempDir.resolve("nonexistent.yml"); Path nonExistentFile = tempDir.resolve("nonexistent.yml");
assertThrows(IOException.class, () -> { assertThrows(IOException.class, () -> {
configLoader.load(nonExistentFile); configLoader.load(nonExistentFile);
}); });
} }
@Test @Test
void testLoadNullPath() { void testLoadNullPath() {
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> { ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(null); configLoader.load(null);
}); });
assertTrue(exception.getMessage().contains("cannot be null")); assertTrue(exception.getMessage().contains("cannot be null"));
} }
@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);
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> { ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile); configLoader.load(configFile);
}); });
assertTrue(exception.getMessage().contains("No selection coordinates found")); assertTrue(exception.getMessage().contains("No selection coordinates found"));
} }
@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);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> { ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile); configLoader.load(configFile);
}); });
assertTrue(exception.getMessage().contains("must be in pairs")); assertTrue(exception.getMessage().contains("must be in pairs"));
} }
} }
@ -198,146 +197,146 @@ 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);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> { ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile); configLoader.load(configFile);
}); });
assertTrue(exception.getMessage().contains("Missing required 'contents' section")); assertTrue(exception.getMessage().contains("Missing required 'contents' section"));
} }
} }
@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);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> { ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile); configLoader.load(configFile);
}); });
assertTrue(exception.getMessage().contains("Unknown material")); assertTrue(exception.getMessage().contains("Unknown material"));
} }
} }
@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);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> { ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile); configLoader.load(configFile);
}); });
assertTrue(exception.getMessage().contains("must be positive")); assertTrue(exception.getMessage().contains("must be positive"));
} }
} }
@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);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> { ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile); configLoader.load(configFile);
}); });
assertTrue(exception.getMessage().contains("between 0 and 100")); assertTrue(exception.getMessage().contains("between 0 and 100"));
} }
} }
@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);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class); Server server = mock(Server.class);
World world = mock(World.class); World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server); bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
MineConfig config = configLoader.load(configFile); MineConfig config = configLoader.load(configFile);
assertTrue(config.isTeleportOnReset()); assertTrue(config.isTeleportOnReset());
assertNotNull(config.getTeleportLocation()); assertNotNull(config.getTeleportLocation());
assertEquals(5.0, config.getTeleportLocation().getX()); assertEquals(5.0, config.getTeleportLocation().getX());
@ -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,18 +9,20 @@ 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.
* Verifies that cache invalidation works correctly when statistics change. * Verifies that cache invalidation works correctly when statistics change.
*/ */
class LeaderboardCacheIntegrationTest { class LeaderboardCacheIntegrationTest {
private StatsManager statsManager; private StatsManager statsManager;
private Leaderboard leaderboard; private Leaderboard leaderboard;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
// Create a mock plugin // Create a mock plugin
@ -30,54 +32,54 @@ class LeaderboardCacheIntegrationTest {
when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder); when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder);
when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger")); when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger"));
when(mockPlugin.loLogger()).thenReturn(loLogger); when(mockPlugin.loLogger()).thenReturn(loLogger);
statsManager = new StatsManager(mockPlugin); statsManager = new StatsManager(mockPlugin);
leaderboard = statsManager.getLeaderboard(); leaderboard = statsManager.getLeaderboard();
} }
@Test @Test
void testCacheInvalidatedOnIncrementBlocks() { void testCacheInvalidatedOnIncrementBlocks() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
// Set initial stats // Set initial stats
statsManager.getOrCreate(player1).setTotalBlocks(100); statsManager.getOrCreate(player1).setTotalBlocks(100);
// Build cache // Build cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
assertEquals(1, top1.size(), "Should have 1 player initially"); assertEquals(1, top1.size(), "Should have 1 player initially");
assertEquals(player1, top1.get(0).getPlayerId()); assertEquals(player1, top1.get(0).getPlayerId());
// Increment blocks for player2 - this should invalidate cache // Increment blocks for player2 - this should invalidate cache
statsManager.incrementBlocks(player2, "mine1"); statsManager.incrementBlocks(player2, "mine1");
// Get top again - should rebuild cache with new player // Get top again - should rebuild cache with new player
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(2, top2.size(), "Should have 2 players after increment"); assertEquals(2, top2.size(), "Should have 2 players after increment");
// Verify order (player1 has 100, player2 has 1) // Verify order (player1 has 100, player2 has 1)
assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should still be first"); assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should still be first");
assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second"); assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second");
} }
@Test @Test
void testCacheRebuildsWithCorrectOrder() { void testCacheRebuildsWithCorrectOrder() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
// Set initial stats // Set initial stats
statsManager.getOrCreate(player1).setTotalBlocks(50); statsManager.getOrCreate(player1).setTotalBlocks(50);
statsManager.getOrCreate(player2).setTotalBlocks(100); statsManager.getOrCreate(player2).setTotalBlocks(100);
// Build cache // Build cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
assertEquals(player2, top1.get(0).getPlayerId(), "Player2 should be first initially"); assertEquals(player2, top1.get(0).getPlayerId(), "Player2 should be first initially");
// Increment player1 many times to overtake player2 // Increment player1 many times to overtake player2
for (int i = 0; i < 60; i++) { for (int i = 0; i < 60; i++) {
statsManager.incrementBlocks(player1, "mine1"); statsManager.incrementBlocks(player1, "mine1");
} }
// Get top again - cache should be rebuilt with new order // Get top again - cache should be rebuilt with new order
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should be first after increments"); assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should be first after increments");
@ -85,26 +87,26 @@ class LeaderboardCacheIntegrationTest {
assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second"); assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second");
assertEquals(100, top2.get(1).getCount(), "Player2 should still have 100 blocks"); assertEquals(100, top2.get(1).getCount(), "Player2 should still have 100 blocks");
} }
@Test @Test
void testMultipleIncrementsInvalidateCache() { void testMultipleIncrementsInvalidateCache() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
// Build empty cache // Build empty cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
assertTrue(top1.isEmpty(), "Should be empty initially"); assertTrue(top1.isEmpty(), "Should be empty initially");
// Increment blocks multiple times // Increment blocks multiple times
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
statsManager.incrementBlocks(player1, "mine1"); statsManager.incrementBlocks(player1, "mine1");
} }
// Get top - should show updated count // Get top - should show updated count
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(1, top2.size()); assertEquals(1, top2.size());
assertEquals(5, top2.get(0).getCount(), "Should have 5 blocks after 5 increments"); assertEquals(5, top2.get(0).getCount(), "Should have 5 blocks after 5 increments");
} }
@Test @Test
void testCachePerformance() { void testCachePerformance() {
// Add many players // Add many players
@ -112,39 +114,39 @@ class LeaderboardCacheIntegrationTest {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(i + 1); statsManager.getOrCreate(playerId).setTotalBlocks(i + 1);
} }
// First call - builds cache (slower) // First call - builds cache (slower)
long start1 = System.nanoTime(); long start1 = System.nanoTime();
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
long time1 = System.nanoTime() - start1; long time1 = System.nanoTime() - start1;
// Second call - uses cache (should be much faster) // Second call - uses cache (should be much faster)
long start2 = System.nanoTime(); long start2 = System.nanoTime();
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
long time2 = System.nanoTime() - start2; long time2 = System.nanoTime() - start2;
assertEquals(10, top1.size()); assertEquals(10, top1.size());
assertEquals(10, top2.size()); assertEquals(10, top2.size());
// 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
void testDifferentLimitsUseSameCache() { void testDifferentLimitsUseSameCache() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID(); UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(100); statsManager.getOrCreate(player1).setTotalBlocks(100);
statsManager.getOrCreate(player2).setTotalBlocks(200); statsManager.getOrCreate(player2).setTotalBlocks(200);
statsManager.getOrCreate(player3).setTotalBlocks(300); statsManager.getOrCreate(player3).setTotalBlocks(300);
// Build cache with limit 10 // Build cache with limit 10
List<LeaderboardEntry> top10 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top10 = leaderboard.getTopTotal(10);
assertEquals(3, top10.size()); assertEquals(3, top10.size());
// Get with limit 2 - should use same cache // Get with limit 2 - should use same cache
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(2); List<LeaderboardEntry> top2 = leaderboard.getTopTotal(2);
assertEquals(2, top2.size()); assertEquals(2, top2.size());

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,18 +9,20 @@ 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.
* Tests caching behavior, sorting, and filtering. * Tests caching behavior, sorting, and filtering.
*/ */
class LeaderboardTest { class LeaderboardTest {
private StatsManager statsManager; private StatsManager statsManager;
private Leaderboard leaderboard; private Leaderboard leaderboard;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
// Create a mock plugin // Create a mock plugin
@ -30,175 +32,175 @@ class LeaderboardTest {
when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder); when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder);
when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger")); when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger"));
when(mockPlugin.loLogger()).thenReturn(loLogger); when(mockPlugin.loLogger()).thenReturn(loLogger);
statsManager = new StatsManager(mockPlugin); statsManager = new StatsManager(mockPlugin);
leaderboard = statsManager.getLeaderboard(); leaderboard = statsManager.getLeaderboard();
} }
@Test @Test
void testGetTopTotal_EmptyStats() { void testGetTopTotal_EmptyStats() {
List<LeaderboardEntry> top = leaderboard.getTopTotal(10); List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertTrue(top.isEmpty(), "Top list should be empty when no stats exist"); assertTrue(top.isEmpty(), "Top list should be empty when no stats exist");
} }
@Test @Test
void testGetTopTotal_SinglePlayer() { void testGetTopTotal_SinglePlayer() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
PlayerStats stats = statsManager.getOrCreate(playerId); PlayerStats stats = statsManager.getOrCreate(playerId);
stats.setTotalBlocks(100); stats.setTotalBlocks(100);
List<LeaderboardEntry> top = leaderboard.getTopTotal(10); List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(1, top.size()); assertEquals(1, top.size());
assertEquals(playerId, top.get(0).getPlayerId()); assertEquals(playerId, top.get(0).getPlayerId());
assertEquals(100, top.get(0).getCount()); assertEquals(100, top.get(0).getCount());
} }
@Test @Test
void testGetTopTotal_SortedDescending() { void testGetTopTotal_SortedDescending() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID(); UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(50); statsManager.getOrCreate(player1).setTotalBlocks(50);
statsManager.getOrCreate(player2).setTotalBlocks(200); statsManager.getOrCreate(player2).setTotalBlocks(200);
statsManager.getOrCreate(player3).setTotalBlocks(100); statsManager.getOrCreate(player3).setTotalBlocks(100);
List<LeaderboardEntry> top = leaderboard.getTopTotal(10); List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(3, top.size()); assertEquals(3, top.size());
assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks
assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks
assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks
} }
@Test @Test
void testGetTopTotal_LimitRespected() { void testGetTopTotal_LimitRespected() {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(i + 1); statsManager.getOrCreate(playerId).setTotalBlocks(i + 1);
} }
List<LeaderboardEntry> top = leaderboard.getTopTotal(5); List<LeaderboardEntry> top = leaderboard.getTopTotal(5);
assertEquals(5, top.size(), "Should respect the limit parameter"); assertEquals(5, top.size(), "Should respect the limit parameter");
} }
@Test @Test
void testGetTopTotal_FiltersZeroBlocks() { void testGetTopTotal_FiltersZeroBlocks() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(100); statsManager.getOrCreate(player1).setTotalBlocks(100);
statsManager.getOrCreate(player2).setTotalBlocks(0); statsManager.getOrCreate(player2).setTotalBlocks(0);
List<LeaderboardEntry> top = leaderboard.getTopTotal(10); List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(1, top.size(), "Should filter out players with 0 blocks"); assertEquals(1, top.size(), "Should filter out players with 0 blocks");
assertEquals(player1, top.get(0).getPlayerId()); assertEquals(player1, top.get(0).getPlayerId());
} }
@Test @Test
void testGetTopTotal_CachingWorks() { void testGetTopTotal_CachingWorks() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(100); statsManager.getOrCreate(playerId).setTotalBlocks(100);
// First call - builds cache // First call - builds cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
// Second call - should use cache (same reference) // Second call - should use cache (same reference)
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10); List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(top1.size(), top2.size()); assertEquals(top1.size(), top2.size());
assertEquals(top1.get(0).getPlayerId(), top2.get(0).getPlayerId()); assertEquals(top1.get(0).getPlayerId(), top2.get(0).getPlayerId());
} }
@Test @Test
void testInvalidateCache_ClearsCache() { void testInvalidateCache_ClearsCache() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(100); statsManager.getOrCreate(playerId).setTotalBlocks(100);
// Build cache // Build cache
leaderboard.getTopTotal(10); leaderboard.getTopTotal(10);
// Invalidate cache // Invalidate cache
leaderboard.invalidateCache(); leaderboard.invalidateCache();
// Add new player // Add new player
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player2).setTotalBlocks(200); statsManager.getOrCreate(player2).setTotalBlocks(200);
// Should rebuild cache with new player // Should rebuild cache with new player
List<LeaderboardEntry> top = leaderboard.getTopTotal(10); List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(2, top.size(), "Cache should be rebuilt after invalidation"); assertEquals(2, top.size(), "Cache should be rebuilt after invalidation");
assertEquals(player2, top.get(0).getPlayerId(), "New player should be first"); assertEquals(player2, top.get(0).getPlayerId(), "New player should be first");
} }
@Test @Test
void testGetTopByMine_SortedDescending() { void testGetTopByMine_SortedDescending() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID(); UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setMineBlocks("mine1", 50); statsManager.getOrCreate(player1).setMineBlocks("mine1", 50);
statsManager.getOrCreate(player2).setMineBlocks("mine1", 200); statsManager.getOrCreate(player2).setMineBlocks("mine1", 200);
statsManager.getOrCreate(player3).setMineBlocks("mine1", 100); statsManager.getOrCreate(player3).setMineBlocks("mine1", 100);
List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10); List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10);
assertEquals(3, top.size()); assertEquals(3, top.size());
assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks
assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks
assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks
} }
@Test @Test
void testGetTopByMine_FiltersZeroBlocks() { void testGetTopByMine_FiltersZeroBlocks() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player1).setMineBlocks("mine1", 100); statsManager.getOrCreate(player1).setMineBlocks("mine1", 100);
statsManager.getOrCreate(player2).setMineBlocks("mine1", 0); statsManager.getOrCreate(player2).setMineBlocks("mine1", 0);
List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10); List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10);
assertEquals(1, top.size(), "Should filter out players with 0 blocks"); assertEquals(1, top.size(), "Should filter out players with 0 blocks");
assertEquals(player1, top.get(0).getPlayerId()); assertEquals(player1, top.get(0).getPlayerId());
} }
@Test @Test
void testGetTopByMine_OnlyIncludesSpecificMine() { void testGetTopByMine_OnlyIncludesSpecificMine() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player1).setMineBlocks("mine1", 100); statsManager.getOrCreate(player1).setMineBlocks("mine1", 100);
statsManager.getOrCreate(player2).setMineBlocks("mine2", 200); statsManager.getOrCreate(player2).setMineBlocks("mine2", 200);
List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10); List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10);
assertEquals(1, top.size(), "Should only include players with blocks in specified mine"); assertEquals(1, top.size(), "Should only include players with blocks in specified mine");
assertEquals(player1, top.get(0).getPlayerId()); assertEquals(player1, top.get(0).getPlayerId());
} }
@Test @Test
void testGetPosition_ReturnsCorrectPosition() { void testGetPosition_ReturnsCorrectPosition() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID(); UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(50); statsManager.getOrCreate(player1).setTotalBlocks(50);
statsManager.getOrCreate(player2).setTotalBlocks(200); statsManager.getOrCreate(player2).setTotalBlocks(200);
statsManager.getOrCreate(player3).setTotalBlocks(100); statsManager.getOrCreate(player3).setTotalBlocks(100);
assertEquals(1, leaderboard.getPosition(player2), "Player with most blocks should be position 1"); assertEquals(1, leaderboard.getPosition(player2), "Player with most blocks should be position 1");
assertEquals(2, leaderboard.getPosition(player3), "Player with second most blocks should be position 2"); assertEquals(2, leaderboard.getPosition(player3), "Player with second most blocks should be position 2");
assertEquals(3, leaderboard.getPosition(player1), "Player with least blocks should be position 3"); assertEquals(3, leaderboard.getPosition(player1), "Player with least blocks should be position 3");
} }
@Test @Test
void testGetPosition_ReturnsMinusOneForNonExistentPlayer() { void testGetPosition_ReturnsMinusOneForNonExistentPlayer() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for non-existent player"); assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for non-existent player");
} }
@Test @Test
void testGetPosition_ReturnsMinusOneForZeroBlocks() { void testGetPosition_ReturnsMinusOneForZeroBlocks() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(0); statsManager.getOrCreate(playerId).setTotalBlocks(0);
assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for player with 0 blocks"); assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for player with 0 blocks");
} }
} }

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;
@ -25,51 +25,50 @@ import static org.mockito.Mockito.*;
* Tests loading, saving, and auto-saving of player statistics. * Tests loading, saving, and auto-saving of player statistics.
*/ */
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);
LoLogger loLogger = mock(LoLogger.class); LoLogger loLogger = mock(LoLogger.class);
scheduler = mock(Scheduler.class); scheduler = mock(Scheduler.class);
when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); when(plugin.getDataFolder()).thenReturn(tempDir.toFile());
when(plugin.getLogger()).thenReturn(Logger.getLogger("TestLogger")); when(plugin.getLogger()).thenReturn(Logger.getLogger("TestLogger"));
when(plugin.loLogger()).thenReturn(loLogger); when(plugin.loLogger()).thenReturn(loLogger);
statsManager = new StatsManager(plugin); statsManager = new StatsManager(plugin);
} }
@Test @Test
void testGetOrCreate() { void testGetOrCreate() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
PlayerStats stats1 = statsManager.getOrCreate(playerId); PlayerStats stats1 = statsManager.getOrCreate(playerId);
assertNotNull(stats1); assertNotNull(stats1);
assertEquals(playerId, stats1.getPlayerId()); assertEquals(playerId, stats1.getPlayerId());
// Should return the same instance // Should return the same instance
PlayerStats stats2 = statsManager.getOrCreate(playerId); PlayerStats stats2 = statsManager.getOrCreate(playerId);
assertSame(stats1, stats2); assertSame(stats1, stats2);
} }
@Test @Test
void testIncrementBlocks() { void testIncrementBlocks() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
String mineName = "TestMine"; String mineName = "TestMine";
statsManager.incrementBlocks(playerId, mineName); statsManager.incrementBlocks(playerId, mineName);
PlayerStats stats = statsManager.getOrCreate(playerId); PlayerStats stats = statsManager.getOrCreate(playerId);
assertEquals(1, stats.getTotalBlocks()); assertEquals(1, stats.getTotalBlocks());
assertEquals(1, stats.getMineBlocks(mineName)); assertEquals(1, stats.getMineBlocks(mineName));
// Increment again // Increment again
statsManager.incrementBlocks(playerId, mineName); statsManager.incrementBlocks(playerId, mineName);
assertEquals(2, stats.getTotalBlocks()); assertEquals(2, stats.getTotalBlocks());
@ -79,85 +78,85 @@ class StatsManagerTest {
@Test @Test
void testIncrementBlocksMultipleMines() { void testIncrementBlocksMultipleMines() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
statsManager.incrementBlocks(playerId, "Mine1"); statsManager.incrementBlocks(playerId, "Mine1");
statsManager.incrementBlocks(playerId, "Mine2"); statsManager.incrementBlocks(playerId, "Mine2");
statsManager.incrementBlocks(playerId, "Mine1"); statsManager.incrementBlocks(playerId, "Mine1");
PlayerStats stats = statsManager.getOrCreate(playerId); PlayerStats stats = statsManager.getOrCreate(playerId);
assertEquals(3, stats.getTotalBlocks()); assertEquals(3, stats.getTotalBlocks());
assertEquals(2, stats.getMineBlocks("Mine1")); assertEquals(2, stats.getMineBlocks("Mine1"));
assertEquals(1, stats.getMineBlocks("Mine2")); assertEquals(1, stats.getMineBlocks("Mine2"));
} }
@Test @Test
void testSaveEmptyStats() throws IOException { void testSaveEmptyStats() throws IOException {
statsManager.save(); statsManager.save();
Path statsFile = tempDir.resolve("stats.yml"); Path statsFile = tempDir.resolve("stats.yml");
assertTrue(Files.exists(statsFile)); assertTrue(Files.exists(statsFile));
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(statsFile.toFile()); YamlConfiguration yaml = YamlConfiguration.loadConfiguration(statsFile.toFile());
assertNull(yaml.getConfigurationSection("players")); assertNull(yaml.getConfigurationSection("players"));
} }
@Test @Test
void testSaveWithStats() throws IOException { void testSaveWithStats() throws IOException {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
statsManager.incrementBlocks(playerId, "TestMine"); statsManager.incrementBlocks(playerId, "TestMine");
statsManager.incrementBlocks(playerId, "TestMine"); statsManager.incrementBlocks(playerId, "TestMine");
statsManager.incrementBlocks(playerId, "AnotherMine"); statsManager.incrementBlocks(playerId, "AnotherMine");
statsManager.save(); statsManager.save();
Path statsFile = tempDir.resolve("stats.yml"); Path statsFile = tempDir.resolve("stats.yml");
assertTrue(Files.exists(statsFile)); assertTrue(Files.exists(statsFile));
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"));
} }
@Test @Test
void testLoadNonExistentFile() throws IOException { void testLoadNonExistentFile() throws IOException {
// Should not throw exception, just log info // Should not throw exception, just log info
statsManager.load(); statsManager.load();
// Stats should be empty // Stats should be empty
assertTrue(statsManager.getAllStats().isEmpty()); assertTrue(statsManager.getAllStats().isEmpty());
} }
@Test @Test
void testLoadEmptyFile() throws IOException { void testLoadEmptyFile() throws IOException {
Path statsFile = tempDir.resolve("stats.yml"); Path statsFile = tempDir.resolve("stats.yml");
Files.writeString(statsFile, ""); Files.writeString(statsFile, "");
statsManager.load(); statsManager.load();
assertTrue(statsManager.getAllStats().isEmpty()); assertTrue(statsManager.getAllStats().isEmpty());
} }
@Test @Test
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);
statsManager.load(); statsManager.load();
PlayerStats stats = statsManager.getOrCreate(playerId); PlayerStats stats = statsManager.getOrCreate(playerId);
assertEquals(100, stats.getTotalBlocks()); assertEquals(100, stats.getTotalBlocks());
assertEquals(60, stats.getMineBlocks("mine1")); assertEquals(60, stats.getMineBlocks("mine1"));
@ -167,93 +166,93 @@ 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);
// Should not throw exception, just log warning // Should not throw exception, just log warning
statsManager.load(); statsManager.load();
assertTrue(statsManager.getAllStats().isEmpty()); assertTrue(statsManager.getAllStats().isEmpty());
} }
@Test @Test
void testSaveAndLoadRoundTrip() throws IOException { void testSaveAndLoadRoundTrip() throws IOException {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
statsManager.incrementBlocks(player1, "Mine1"); statsManager.incrementBlocks(player1, "Mine1");
statsManager.incrementBlocks(player1, "Mine1"); statsManager.incrementBlocks(player1, "Mine1");
statsManager.incrementBlocks(player1, "Mine2"); statsManager.incrementBlocks(player1, "Mine2");
statsManager.incrementBlocks(player2, "Mine1"); statsManager.incrementBlocks(player2, "Mine1");
statsManager.save(); statsManager.save();
// Create new manager and load // Create new manager and load
StatsManager newManager = new StatsManager(plugin); StatsManager newManager = new StatsManager(plugin);
newManager.load(); newManager.load();
PlayerStats stats1 = newManager.getOrCreate(player1); PlayerStats stats1 = newManager.getOrCreate(player1);
assertEquals(3, stats1.getTotalBlocks()); assertEquals(3, stats1.getTotalBlocks());
assertEquals(2, stats1.getMineBlocks("Mine1")); assertEquals(2, stats1.getMineBlocks("Mine1"));
assertEquals(1, stats1.getMineBlocks("Mine2")); assertEquals(1, stats1.getMineBlocks("Mine2"));
PlayerStats stats2 = newManager.getOrCreate(player2); PlayerStats stats2 = newManager.getOrCreate(player2);
assertEquals(1, stats2.getTotalBlocks()); assertEquals(1, stats2.getTotalBlocks());
assertEquals(1, stats2.getMineBlocks("Mine1")); assertEquals(1, stats2.getMineBlocks("Mine1"));
} }
@Test @Test
void testStartAutoSave() { void testStartAutoSave() {
try (MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { try (MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
ScheduledTask task = mock(ScheduledTask.class); ScheduledTask task = mock(ScheduledTask.class);
schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler); schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task); when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task);
statsManager.startAutoSave(); statsManager.startAutoSave();
// Verify that runTimerAsync was called with correct interval (5 minutes = 6000 ticks) // Verify that runTimerAsync was called with correct interval (5 minutes = 6000 ticks)
verify(scheduler).runTimerAsync(any(Runnable.class), eq(6000L), eq(6000L)); verify(scheduler).runTimerAsync(any(Runnable.class), eq(6000L), eq(6000L));
} }
} }
@Test @Test
void testStopAutoSave() { void testStopAutoSave() {
try (MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) { try (MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
ScheduledTask task = mock(ScheduledTask.class); ScheduledTask task = mock(ScheduledTask.class);
schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler); schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task); when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task);
statsManager.startAutoSave(); statsManager.startAutoSave();
statsManager.stopAutoSave(); statsManager.stopAutoSave();
verify(task).cancel(); verify(task).cancel();
} }
} }
@Test @Test
void testStopAutoSaveWithoutStart() { void testStopAutoSaveWithoutStart() {
// Should not throw exception // Should not throw exception
assertDoesNotThrow(() -> statsManager.stopAutoSave()); assertDoesNotThrow(() -> statsManager.stopAutoSave());
} }
@Test @Test
void testGetAllStats() { void testGetAllStats() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
statsManager.incrementBlocks(player1, "Mine1"); statsManager.incrementBlocks(player1, "Mine1");
statsManager.incrementBlocks(player2, "Mine2"); statsManager.incrementBlocks(player2, "Mine2");
assertEquals(2, statsManager.getAllStats().size()); assertEquals(2, statsManager.getAllStats().size());
} }
@Test @Test
void testGetLeaderboard() { void testGetLeaderboard() {
assertNotNull(statsManager.getLeaderboard()); assertNotNull(statsManager.getLeaderboard());

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.*;
@ -20,135 +21,135 @@ import static org.mockito.Mockito.*;
* Tests Paper detection and chunk update functionality. * Tests Paper detection and chunk update functionality.
*/ */
class ChunkUtilsTest { class ChunkUtilsTest {
@Mock @Mock
private World world; private World world;
@Mock @Mock
private Chunk chunk; private Chunk chunk;
@Mock @Mock
private Player player; private Player player;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
MockitoAnnotations.openMocks(this); MockitoAnnotations.openMocks(this);
} }
@Test @Test
void testUpdateChunks_withNullRegion_throwsException() { void testUpdateChunks_withNullRegion_throwsException() {
assertThrows(IllegalArgumentException.class, () -> { assertThrows(IllegalArgumentException.class, () -> {
ChunkUtils.updateChunks(null); ChunkUtils.updateChunks(null);
}); });
} }
@Test @Test
void testUpdateChunks_withSingleChunkRegion() { void testUpdateChunks_withSingleChunkRegion() {
// Create a small region within a single chunk // Create a small region within a single chunk
Location loc1 = new Location(world, 0, 64, 0); Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 5, 70, 5); Location loc2 = new Location(world, 5, 70, 5);
Cuboid region = new Cuboid(loc1, loc2); Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.singletonList(player)); when(world.getPlayers()).thenReturn(Collections.singletonList(player));
when(chunk.getWorld()).thenReturn(world); when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(0); when(chunk.getX()).thenReturn(0);
when(chunk.getZ()).thenReturn(0); when(chunk.getZ()).thenReturn(0);
// Should not throw exception // Should not throw exception
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Verify chunk was retrieved // Verify chunk was retrieved
verify(world, atLeastOnce()).getChunkAt(0, 0); verify(world, atLeastOnce()).getChunkAt(0, 0);
} }
@Test @Test
void testUpdateChunks_withMultipleChunks() { void testUpdateChunks_withMultipleChunks() {
// Create a region spanning multiple chunks (16 blocks per chunk) // Create a region spanning multiple chunks (16 blocks per chunk)
Location loc1 = new Location(world, 0, 64, 0); Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 32, 70, 32); Location loc2 = new Location(world, 32, 70, 32);
Cuboid region = new Cuboid(loc1, loc2); Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.singletonList(player)); when(world.getPlayers()).thenReturn(Collections.singletonList(player));
when(chunk.getWorld()).thenReturn(world); when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(0); when(chunk.getX()).thenReturn(0);
when(chunk.getZ()).thenReturn(0); when(chunk.getZ()).thenReturn(0);
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Should retrieve multiple chunks (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2) // Should retrieve multiple chunks (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)
// That's 9 chunks total for a 3x3 chunk area // That's 9 chunks total for a 3x3 chunk area
verify(world, atLeast(9)).getChunkAt(anyInt(), anyInt()); verify(world, atLeast(9)).getChunkAt(anyInt(), anyInt());
} }
@Test @Test
void testUpdateChunks_withNegativeCoordinates() { void testUpdateChunks_withNegativeCoordinates() {
// Test with negative coordinates // Test with negative coordinates
Location loc1 = new Location(world, -16, 64, -16); Location loc1 = new Location(world, -16, 64, -16);
Location loc2 = new Location(world, -1, 70, -1); Location loc2 = new Location(world, -1, 70, -1);
Cuboid region = new Cuboid(loc1, loc2); Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.singletonList(player)); when(world.getPlayers()).thenReturn(Collections.singletonList(player));
when(chunk.getWorld()).thenReturn(world); when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(-1); when(chunk.getX()).thenReturn(-1);
when(chunk.getZ()).thenReturn(-1); when(chunk.getZ()).thenReturn(-1);
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Verify chunk was retrieved for negative coordinates // Verify chunk was retrieved for negative coordinates
verify(world, atLeastOnce()).getChunkAt(-1, -1); verify(world, atLeastOnce()).getChunkAt(-1, -1);
} }
@Test @Test
void testUpdateChunks_withLargeRegion() { void testUpdateChunks_withLargeRegion() {
// Test with a large region spanning many chunks // Test with a large region spanning many chunks
Location loc1 = new Location(world, 0, 0, 0); Location loc1 = new Location(world, 0, 0, 0);
Location loc2 = new Location(world, 100, 100, 100); Location loc2 = new Location(world, 100, 100, 100);
Cuboid region = new Cuboid(loc1, loc2); Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.emptyList()); when(world.getPlayers()).thenReturn(Collections.emptyList());
when(chunk.getWorld()).thenReturn(world); when(chunk.getWorld()).thenReturn(world);
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Should handle large regions without issues // Should handle large regions without issues
// 100 blocks = ~7 chunks per axis, so ~49 chunks total // 100 blocks = ~7 chunks per axis, so ~49 chunks total
verify(world, atLeast(40)).getChunkAt(anyInt(), anyInt()); verify(world, atLeast(40)).getChunkAt(anyInt(), anyInt());
} }
@Test @Test
void testUpdateChunks_withNoPlayers() { void testUpdateChunks_withNoPlayers() {
// Test behavior when no players are online // Test behavior when no players are online
Location loc1 = new Location(world, 0, 64, 0); Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 5, 70, 5); Location loc2 = new Location(world, 5, 70, 5);
Cuboid region = new Cuboid(loc1, loc2); Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.emptyList()); when(world.getPlayers()).thenReturn(Collections.emptyList());
when(chunk.getWorld()).thenReturn(world); when(chunk.getWorld()).thenReturn(world);
// Should not throw exception even with no players // Should not throw exception even with no players
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
} }
@Test @Test
void testChunkCoordinateCalculation() { void testChunkCoordinateCalculation() {
// Test that chunk coordinates are calculated correctly // Test that chunk coordinates are calculated correctly
// Block 0-15 = chunk 0, block 16-31 = chunk 1, etc. // Block 0-15 = chunk 0, block 16-31 = chunk 1, etc.
Location loc1 = new Location(world, 0, 64, 0); Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 15, 70, 15); Location loc2 = new Location(world, 15, 70, 15);
Cuboid singleChunk = new Cuboid(loc1, loc2); Cuboid singleChunk = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.emptyList()); when(world.getPlayers()).thenReturn(Collections.emptyList());
when(chunk.getWorld()).thenReturn(world); when(chunk.getWorld()).thenReturn(world);
ChunkUtils.updateChunks(singleChunk); ChunkUtils.updateChunks(singleChunk);
// Should only access chunk (0,0) // Should only access chunk (0,0)
verify(world, times(1)).getChunkAt(0, 0); verify(world, times(1)).getChunkAt(0, 0);
} }

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,19 +9,20 @@ 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 {
@Test @Test
void testParseValidLocation() throws ConfigParseException { void testParseValidLocation() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class); World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world"); when(mockWorld.getName()).thenReturn("world");
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld); bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld);
Location location = LocationParser.parse("world;100.5;64.0;-200.75;90.0;45.0"); Location location = LocationParser.parse("world;100.5;64.0;-200.75;90.0;45.0");
assertNotNull(location); assertNotNull(location);
assertEquals(mockWorld, location.getWorld()); assertEquals(mockWorld, location.getWorld());
assertEquals(100.5, location.getX(), 0.001); assertEquals(100.5, location.getX(), 0.001);
@ -31,31 +32,31 @@ class LocationParserTest {
assertEquals(45.0f, location.getPitch(), 0.001); assertEquals(45.0f, location.getPitch(), 0.001);
} }
} }
@Test @Test
void testParseIntegerCoordinates() throws ConfigParseException { void testParseIntegerCoordinates() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class); World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world_nether"); when(mockWorld.getName()).thenReturn("world_nether");
bukkit.when(() -> Bukkit.getWorld("world_nether")).thenReturn(mockWorld); bukkit.when(() -> Bukkit.getWorld("world_nether")).thenReturn(mockWorld);
Location location = LocationParser.parse("world_nether;0;128;0;0;0"); Location location = LocationParser.parse("world_nether;0;128;0;0;0");
assertNotNull(location); assertNotNull(location);
assertEquals(0.0, location.getX(), 0.001); assertEquals(0.0, location.getX(), 0.001);
assertEquals(128.0, location.getY(), 0.001); assertEquals(128.0, location.getY(), 0.001);
assertEquals(0.0, location.getZ(), 0.001); assertEquals(0.0, location.getZ(), 0.001);
} }
} }
@Test @Test
void testParseNegativeCoordinates() throws ConfigParseException { void testParseNegativeCoordinates() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class); World mockWorld = mock(World.class);
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld); bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld);
Location location = LocationParser.parse("world;-100;-64;-200;-180;-90"); Location location = LocationParser.parse("world;-100;-64;-200;-180;-90");
assertNotNull(location); assertNotNull(location);
assertEquals(-100.0, location.getX(), 0.001); assertEquals(-100.0, location.getX(), 0.001);
assertEquals(-64.0, location.getY(), 0.001); assertEquals(-64.0, location.getY(), 0.001);
@ -64,111 +65,111 @@ class LocationParserTest {
assertEquals(-90.0f, location.getPitch(), 0.001); assertEquals(-90.0f, location.getPitch(), 0.001);
} }
} }
@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"));
} }
@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"));
} }
@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"));
} }
@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"));
} }
@Test @Test
void testParseNonExistentWorld() { void testParseNonExistentWorld() {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
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"));
} }
} }
@Test @Test
void testFormatValidLocation() { void testFormatValidLocation() {
World mockWorld = mock(World.class); World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world"); when(mockWorld.getName()).thenReturn("world");
Location location = new Location(mockWorld, 100.5, 64.0, -200.75, 90.0f, 45.0f); Location location = new Location(mockWorld, 100.5, 64.0, -200.75, 90.0f, 45.0f);
String formatted = LocationParser.format(location); String formatted = LocationParser.format(location);
assertEquals("world;100.50;64.00;-200.75;90.00;45.00", formatted); assertEquals("world;100.50;64.00;-200.75;90.00;45.00", formatted);
} }
@Test @Test
void testFormatIntegerCoordinates() { void testFormatIntegerCoordinates() {
World mockWorld = mock(World.class); World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world_nether"); when(mockWorld.getName()).thenReturn("world_nether");
Location location = new Location(mockWorld, 0, 128, 0, 0, 0); Location location = new Location(mockWorld, 0, 128, 0, 0, 0);
String formatted = LocationParser.format(location); String formatted = LocationParser.format(location);
assertEquals("world_nether;0.00;128.00;0.00;0.00;0.00", formatted); assertEquals("world_nether;0.00;128.00;0.00;0.00;0.00", formatted);
} }
@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"));
} }
@Test @Test
void testFormatNullWorld() { void testFormatNullWorld() {
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"));
} }
@Test @Test
void testRoundTrip() throws ConfigParseException { void testRoundTrip() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) { try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class); World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world"); when(mockWorld.getName()).thenReturn("world");
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld); bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld);
Location original = new Location(mockWorld, 100.5, 64.0, -200.75, 90.0f, 45.0f); Location original = new Location(mockWorld, 100.5, 64.0, -200.75, 90.0f, 45.0f);
String formatted = LocationParser.format(original); String formatted = LocationParser.format(original);
Location parsed = LocationParser.parse(formatted); Location parsed = LocationParser.parse(formatted);
assertEquals(original.getX(), parsed.getX(), 0.01); assertEquals(original.getX(), parsed.getX(), 0.01);
assertEquals(original.getY(), parsed.getY(), 0.01); assertEquals(original.getY(), parsed.getY(), 0.01);
assertEquals(original.getZ(), parsed.getZ(), 0.01); assertEquals(original.getZ(), parsed.getZ(), 0.01);

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;
@ -9,119 +9,119 @@ import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
class SelectionManagerTest { class SelectionManagerTest {
private SelectionManager manager; private SelectionManager manager;
private UUID playerId; private UUID playerId;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
manager = new SelectionManager(); manager = new SelectionManager();
playerId = UUID.randomUUID(); playerId = UUID.randomUUID();
} }
@Test @Test
void getSelection_createsNewSelectionIfNotExists() { void getSelection_createsNewSelectionIfNotExists() {
Selection selection = manager.getSelection(playerId); Selection selection = manager.getSelection(playerId);
assertNotNull(selection); assertNotNull(selection);
assertTrue(manager.hasSelection(playerId)); assertTrue(manager.hasSelection(playerId));
} }
@Test @Test
void getSelection_returnsSameInstanceOnMultipleCalls() { void getSelection_returnsSameInstanceOnMultipleCalls() {
Selection first = manager.getSelection(playerId); Selection first = manager.getSelection(playerId);
Selection second = manager.getSelection(playerId); Selection second = manager.getSelection(playerId);
assertSame(first, second); assertSame(first, second);
} }
@Test @Test
void getSelection_throwsExceptionForNullPlayerId() { void getSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> { assertThrows(IllegalArgumentException.class, () -> {
manager.getSelection(null); manager.getSelection(null);
}); });
} }
@Test @Test
void findSelection_returnsEmptyWhenNotExists() { void findSelection_returnsEmptyWhenNotExists() {
Optional<Selection> result = manager.findSelection(playerId); Optional<Selection> result = manager.findSelection(playerId);
assertTrue(result.isEmpty()); assertTrue(result.isEmpty());
} }
@Test @Test
void findSelection_returnsSelectionWhenExists() { void findSelection_returnsSelectionWhenExists() {
Selection created = manager.getSelection(playerId); Selection created = manager.getSelection(playerId);
Optional<Selection> found = manager.findSelection(playerId); Optional<Selection> found = manager.findSelection(playerId);
assertTrue(found.isPresent()); assertTrue(found.isPresent());
assertSame(created, found.get()); assertSame(created, found.get());
} }
@Test @Test
void findSelection_throwsExceptionForNullPlayerId() { void findSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> { assertThrows(IllegalArgumentException.class, () -> {
manager.findSelection(null); manager.findSelection(null);
}); });
} }
@Test @Test
void hasSelection_returnsFalseWhenNotExists() { void hasSelection_returnsFalseWhenNotExists() {
assertFalse(manager.hasSelection(playerId)); assertFalse(manager.hasSelection(playerId));
} }
@Test @Test
void hasSelection_returnsTrueWhenExists() { void hasSelection_returnsTrueWhenExists() {
manager.getSelection(playerId); manager.getSelection(playerId);
assertTrue(manager.hasSelection(playerId)); assertTrue(manager.hasSelection(playerId));
} }
@Test @Test
void hasSelection_throwsExceptionForNullPlayerId() { void hasSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> { assertThrows(IllegalArgumentException.class, () -> {
manager.hasSelection(null); manager.hasSelection(null);
}); });
} }
@Test @Test
void clearSelection_removesSelection() { void clearSelection_removesSelection() {
manager.getSelection(playerId); manager.getSelection(playerId);
assertTrue(manager.hasSelection(playerId)); assertTrue(manager.hasSelection(playerId));
manager.clearSelection(playerId); manager.clearSelection(playerId);
assertFalse(manager.hasSelection(playerId)); assertFalse(manager.hasSelection(playerId));
} }
@Test @Test
void clearSelection_doesNothingWhenSelectionNotExists() { void clearSelection_doesNothingWhenSelectionNotExists() {
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
manager.clearSelection(playerId); manager.clearSelection(playerId);
}); });
} }
@Test @Test
void clearSelection_throwsExceptionForNullPlayerId() { void clearSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> { assertThrows(IllegalArgumentException.class, () -> {
manager.clearSelection(null); manager.clearSelection(null);
}); });
} }
@Test @Test
void multiplePlayersHaveIndependentSelections() { void multiplePlayersHaveIndependentSelections() {
UUID player1 = UUID.randomUUID(); UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID(); UUID player2 = UUID.randomUUID();
Selection selection1 = manager.getSelection(player1); Selection selection1 = manager.getSelection(player1);
Selection selection2 = manager.getSelection(player2); Selection selection2 = manager.getSelection(player2);
assertNotSame(selection1, selection2); assertNotSame(selection1, selection2);
assertTrue(manager.hasSelection(player1)); assertTrue(manager.hasSelection(player1));
assertTrue(manager.hasSelection(player2)); assertTrue(manager.hasSelection(player2));
manager.clearSelection(player1); manager.clearSelection(player1);
assertFalse(manager.hasSelection(player1)); assertFalse(manager.hasSelection(player1));
assertTrue(manager.hasSelection(player2)); assertTrue(manager.hasSelection(player2));
} }

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;
@ -13,207 +13,207 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
class SelectionTest { class SelectionTest {
private World world; private World world;
private Selection selection; private Selection selection;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
world = mock(World.class); world = mock(World.class);
selection = new Selection(); selection = new Selection();
when(world.getName()).thenReturn("world"); when(world.getName()).thenReturn("world");
} }
@Test @Test
void testSetAndGetPoint() { void testSetAndGetPoint() {
Location loc = new Location(world, 10, 64, 20); Location loc = new Location(world, 10, 64, 20);
selection.setPoint(0, loc); selection.setPoint(0, loc);
Optional<Location> result = selection.getPoint(0); Optional<Location> result = selection.getPoint(0);
assertTrue(result.isPresent()); assertTrue(result.isPresent());
assertEquals(loc, result.get()); assertEquals(loc, result.get());
} }
@Test @Test
void testGetPointReturnsEmptyWhenNotSet() { void testGetPointReturnsEmptyWhenNotSet() {
Optional<Location> result = selection.getPoint(5); Optional<Location> result = selection.getPoint(5);
assertFalse(result.isPresent()); assertFalse(result.isPresent());
} }
@Test @Test
void testSetPointInvalidIndexThrows() { void testSetPointInvalidIndexThrows() {
Location loc = new Location(world, 10, 64, 20); Location loc = new Location(world, 10, 64, 20);
assertThrows(IllegalArgumentException.class, () -> selection.setPoint(-1, loc)); assertThrows(IllegalArgumentException.class, () -> selection.setPoint(-1, loc));
assertThrows(IllegalArgumentException.class, () -> selection.setPoint(10, loc)); assertThrows(IllegalArgumentException.class, () -> selection.setPoint(10, loc));
} }
@Test @Test
void testGetPointInvalidIndexThrows() { void testGetPointInvalidIndexThrows() {
assertThrows(IllegalArgumentException.class, () -> selection.getPoint(-1)); assertThrows(IllegalArgumentException.class, () -> selection.getPoint(-1));
assertThrows(IllegalArgumentException.class, () -> selection.getPoint(10)); assertThrows(IllegalArgumentException.class, () -> selection.getPoint(10));
} }
@Test @Test
void testGetPair() { void testGetPair() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30); Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
selection.setPoint(1, loc2); selection.setPoint(1, loc2);
Location[] pair = selection.getPair(0); Location[] pair = selection.getPair(0);
assertEquals(2, pair.length); assertEquals(2, pair.length);
assertEquals(loc1, pair[0]); assertEquals(loc1, pair[0]);
assertEquals(loc2, pair[1]); assertEquals(loc2, pair[1]);
} }
@Test @Test
void testGetPairWithNulls() { void testGetPairWithNulls() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
selection.setPoint(2, loc1); selection.setPoint(2, loc1);
Location[] pair = selection.getPair(1); Location[] pair = selection.getPair(1);
assertEquals(2, pair.length); assertEquals(2, pair.length);
assertEquals(loc1, pair[0]); assertEquals(loc1, pair[0]);
assertNull(pair[1]); assertNull(pair[1]);
} }
@Test @Test
void testGetPairInvalidIndexThrows() { void testGetPairInvalidIndexThrows() {
assertThrows(IllegalArgumentException.class, () -> selection.getPair(-1)); assertThrows(IllegalArgumentException.class, () -> selection.getPair(-1));
assertThrows(IllegalArgumentException.class, () -> selection.getPair(5)); assertThrows(IllegalArgumentException.class, () -> selection.getPair(5));
} }
@Test @Test
void testHasPairReturnsTrueWhenBothSet() { void testHasPairReturnsTrueWhenBothSet() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30); Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
selection.setPoint(1, loc2); selection.setPoint(1, loc2);
assertTrue(selection.hasPair(0)); assertTrue(selection.hasPair(0));
} }
@Test @Test
void testHasPairReturnsFalseWhenOnlyOneSet() { void testHasPairReturnsFalseWhenOnlyOneSet() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
assertFalse(selection.hasPair(0)); assertFalse(selection.hasPair(0));
} }
@Test @Test
void testHasPairReturnsFalseWhenNoneSet() { void testHasPairReturnsFalseWhenNoneSet() {
assertFalse(selection.hasPair(0)); assertFalse(selection.hasPair(0));
} }
@Test @Test
void testHasPairInvalidIndexThrows() { void testHasPairInvalidIndexThrows() {
assertThrows(IllegalArgumentException.class, () -> selection.hasPair(-1)); assertThrows(IllegalArgumentException.class, () -> selection.hasPair(-1));
assertThrows(IllegalArgumentException.class, () -> selection.hasPair(5)); assertThrows(IllegalArgumentException.class, () -> selection.hasPair(5));
} }
@Test @Test
void testClear() { void testClear() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30); Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
selection.setPoint(1, loc2); selection.setPoint(1, loc2);
selection.setPoint(5, loc1); selection.setPoint(5, loc1);
selection.clear(); selection.clear();
assertFalse(selection.getPoint(0).isPresent()); assertFalse(selection.getPoint(0).isPresent());
assertFalse(selection.getPoint(1).isPresent()); assertFalse(selection.getPoint(1).isPresent());
assertFalse(selection.getPoint(5).isPresent()); assertFalse(selection.getPoint(5).isPresent());
} }
@Test @Test
void testToCuboidsWithNoPairs() { void testToCuboidsWithNoPairs() {
List<Cuboid> cuboids = selection.toCuboids(); List<Cuboid> cuboids = selection.toCuboids();
assertTrue(cuboids.isEmpty()); assertTrue(cuboids.isEmpty());
} }
@Test @Test
void testToCuboidsWithOnePair() { void testToCuboidsWithOnePair() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30); Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
selection.setPoint(1, loc2); selection.setPoint(1, loc2);
List<Cuboid> cuboids = selection.toCuboids(); List<Cuboid> cuboids = selection.toCuboids();
assertEquals(1, cuboids.size()); assertEquals(1, cuboids.size());
assertTrue(cuboids.get(0).contains(new Location(world, 15, 69, 25))); assertTrue(cuboids.get(0).contains(new Location(world, 15, 69, 25)));
} }
@Test @Test
void testToCuboidsWithMultiplePairs() { void testToCuboidsWithMultiplePairs() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30); Location loc2 = new Location(world, 20, 74, 30);
Location loc3 = new Location(world, 30, 64, 40); Location loc3 = new Location(world, 30, 64, 40);
Location loc4 = new Location(world, 40, 74, 50); Location loc4 = new Location(world, 40, 74, 50);
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
selection.setPoint(1, loc2); selection.setPoint(1, loc2);
selection.setPoint(2, loc3); selection.setPoint(2, loc3);
selection.setPoint(3, loc4); selection.setPoint(3, loc4);
List<Cuboid> cuboids = selection.toCuboids(); List<Cuboid> cuboids = selection.toCuboids();
assertEquals(2, cuboids.size()); assertEquals(2, cuboids.size());
} }
@Test @Test
void testToCuboidsSkipsIncompletePairs() { void testToCuboidsSkipsIncompletePairs() {
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30); Location loc2 = new Location(world, 20, 74, 30);
Location loc3 = new Location(world, 30, 64, 40); Location loc3 = new Location(world, 30, 64, 40);
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
selection.setPoint(1, loc2); selection.setPoint(1, loc2);
selection.setPoint(2, loc3); selection.setPoint(2, loc3);
// Point 3 not set // Point 3 not set
List<Cuboid> cuboids = selection.toCuboids(); List<Cuboid> cuboids = selection.toCuboids();
assertEquals(1, cuboids.size()); assertEquals(1, cuboids.size());
} }
@Test @Test
void testPairInvariant() { void testPairInvariant() {
// Test that pairs are organized as 2i and 2i+1 // Test that pairs are organized as 2i and 2i+1
Location loc1 = new Location(world, 10, 64, 20); Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30); Location loc2 = new Location(world, 20, 74, 30);
// Pair 0: indices 0 and 1 // Pair 0: indices 0 and 1
selection.setPoint(0, loc1); selection.setPoint(0, loc1);
selection.setPoint(1, loc2); selection.setPoint(1, loc2);
assertTrue(selection.hasPair(0)); assertTrue(selection.hasPair(0));
// Pair 1: indices 2 and 3 // Pair 1: indices 2 and 3
selection.setPoint(2, loc1); selection.setPoint(2, loc1);
selection.setPoint(3, loc2); selection.setPoint(3, loc2);
assertTrue(selection.hasPair(1)); assertTrue(selection.hasPair(1));
// Pair 2: indices 4 and 5 // Pair 2: indices 4 and 5
selection.setPoint(4, loc1); selection.setPoint(4, loc1);
selection.setPoint(5, loc2); selection.setPoint(5, loc2);
assertTrue(selection.hasPair(2)); assertTrue(selection.hasPair(2));
// Pair 3: indices 6 and 7 // Pair 3: indices 6 and 7
selection.setPoint(6, loc1); selection.setPoint(6, loc1);
selection.setPoint(7, loc2); selection.setPoint(7, loc2);
assertTrue(selection.hasPair(3)); assertTrue(selection.hasPair(3));
// Pair 4: indices 8 and 9 // Pair 4: indices 8 and 9
selection.setPoint(8, loc1); selection.setPoint(8, loc1);
selection.setPoint(9, loc2); selection.setPoint(9, loc2);

View file

@ -1,98 +1,99 @@
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 {
@Test @Test
void testFormatMinutesSeconds() { void testFormatMinutesSeconds() {
// 1 minute = 60 seconds = 1200 ticks // 1 minute = 60 seconds = 1200 ticks
assertEquals("01:00", TimeFormatter.format(1200, "mm:ss")); assertEquals("01:00", TimeFormatter.format(1200, "mm:ss"));
// 2 minutes 30 seconds = 150 seconds = 3000 ticks // 2 minutes 30 seconds = 150 seconds = 3000 ticks
assertEquals("02:30", TimeFormatter.format(3000, "mm:ss")); assertEquals("02:30", TimeFormatter.format(3000, "mm:ss"));
// 0 seconds // 0 seconds
assertEquals("00:00", TimeFormatter.format(0, "mm:ss")); assertEquals("00:00", TimeFormatter.format(0, "mm:ss"));
// 59 seconds = 1180 ticks // 59 seconds = 1180 ticks
assertEquals("00:59", TimeFormatter.format(1180, "mm:ss")); assertEquals("00:59", TimeFormatter.format(1180, "mm:ss"));
// 10 minutes 5 seconds = 605 seconds = 12100 ticks // 10 minutes 5 seconds = 605 seconds = 12100 ticks
assertEquals("10:05", TimeFormatter.format(12100, "mm:ss")); assertEquals("10:05", TimeFormatter.format(12100, "mm:ss"));
} }
@Test @Test
void testFormatHoursMinutesSeconds() { void testFormatHoursMinutesSeconds() {
// 1 hour = 3600 seconds = 72000 ticks // 1 hour = 3600 seconds = 72000 ticks
assertEquals("01:00:00", TimeFormatter.format(72000, "hh:mm:ss")); assertEquals("01:00:00", TimeFormatter.format(72000, "hh:mm:ss"));
// 1 hour 30 minutes 45 seconds = 5445 seconds = 108900 ticks // 1 hour 30 minutes 45 seconds = 5445 seconds = 108900 ticks
assertEquals("01:30:45", TimeFormatter.format(108900, "hh:mm:ss")); assertEquals("01:30:45", TimeFormatter.format(108900, "hh:mm:ss"));
// 0 seconds // 0 seconds
assertEquals("00:00:00", TimeFormatter.format(0, "hh:mm:ss")); assertEquals("00:00:00", TimeFormatter.format(0, "hh:mm:ss"));
// 2 hours 5 minutes 3 seconds = 7503 seconds = 150060 ticks // 2 hours 5 minutes 3 seconds = 7503 seconds = 150060 ticks
assertEquals("02:05:03", TimeFormatter.format(150060, "hh:mm:ss")); assertEquals("02:05:03", TimeFormatter.format(150060, "hh:mm:ss"));
// 23 hours 59 minutes 59 seconds = 86399 seconds = 1727980 ticks // 23 hours 59 minutes 59 seconds = 86399 seconds = 1727980 ticks
assertEquals("23:59:59", TimeFormatter.format(1727980, "hh:mm:ss")); assertEquals("23:59:59", TimeFormatter.format(1727980, "hh:mm:ss"));
} }
@Test @Test
void testFormatSeconds() { void testFormatSeconds() {
// 100 ticks = 5 seconds // 100 ticks = 5 seconds
assertEquals("5", TimeFormatter.format(100, "s")); assertEquals("5", TimeFormatter.format(100, "s"));
// 1200 ticks = 60 seconds // 1200 ticks = 60 seconds
assertEquals("60", TimeFormatter.format(1200, "s")); assertEquals("60", TimeFormatter.format(1200, "s"));
// 0 ticks = 0 seconds // 0 ticks = 0 seconds
assertEquals("0", TimeFormatter.format(0, "s")); assertEquals("0", TimeFormatter.format(0, "s"));
// 6000 ticks = 300 seconds (5 minutes) // 6000 ticks = 300 seconds (5 minutes)
assertEquals("300", TimeFormatter.format(6000, "s")); assertEquals("300", TimeFormatter.format(6000, "s"));
} }
@Test @Test
void testFormatMinutes() { void testFormatMinutes() {
// 1200 ticks = 60 seconds = 1 minute // 1200 ticks = 60 seconds = 1 minute
assertEquals("1", TimeFormatter.format(1200, "m")); assertEquals("1", TimeFormatter.format(1200, "m"));
// 6000 ticks = 300 seconds = 5 minutes // 6000 ticks = 300 seconds = 5 minutes
assertEquals("5", TimeFormatter.format(6000, "m")); assertEquals("5", TimeFormatter.format(6000, "m"));
// 0 ticks = 0 minutes // 0 ticks = 0 minutes
assertEquals("0", TimeFormatter.format(0, "m")); assertEquals("0", TimeFormatter.format(0, "m"));
// 72000 ticks = 3600 seconds = 60 minutes // 72000 ticks = 3600 seconds = 60 minutes
assertEquals("60", TimeFormatter.format(72000, "m")); assertEquals("60", TimeFormatter.format(72000, "m"));
// 1100 ticks = 55 seconds = 0 minutes (truncated) // 1100 ticks = 55 seconds = 0 minutes (truncated)
assertEquals("0", TimeFormatter.format(1100, "m")); assertEquals("0", TimeFormatter.format(1100, "m"));
} }
@Test @Test
void testFormatHours() { void testFormatHours() {
// 72000 ticks = 3600 seconds = 1 hour // 72000 ticks = 3600 seconds = 1 hour
assertEquals("1", TimeFormatter.format(72000, "h")); assertEquals("1", TimeFormatter.format(72000, "h"));
// 144000 ticks = 7200 seconds = 2 hours // 144000 ticks = 7200 seconds = 2 hours
assertEquals("2", TimeFormatter.format(144000, "h")); assertEquals("2", TimeFormatter.format(144000, "h"));
// 0 ticks = 0 hours // 0 ticks = 0 hours
assertEquals("0", TimeFormatter.format(0, "h")); assertEquals("0", TimeFormatter.format(0, "h"));
// 360000 ticks = 18000 seconds = 5 hours // 360000 ticks = 18000 seconds = 5 hours
assertEquals("5", TimeFormatter.format(360000, "h")); assertEquals("5", TimeFormatter.format(360000, "h"));
// 71999 ticks = 3599 seconds = 0 hours (truncated) // 71999 ticks = 3599 seconds = 0 hours (truncated)
assertEquals("0", TimeFormatter.format(71999, "h")); assertEquals("0", TimeFormatter.format(71999, "h"));
} }
@Test @Test
void testFormatCaseInsensitive() { void testFormatCaseInsensitive() {
assertEquals("01:00", TimeFormatter.format(1200, "MM:SS")); assertEquals("01:00", TimeFormatter.format(1200, "MM:SS"));
@ -101,7 +102,7 @@ class TimeFormatterTest {
assertEquals("1", TimeFormatter.format(1200, "M")); assertEquals("1", TimeFormatter.format(1200, "M"));
assertEquals("1", TimeFormatter.format(72000, "H")); assertEquals("1", TimeFormatter.format(72000, "H"));
} }
@Test @Test
void testFormatNegativeTicks() { void testFormatNegativeTicks() {
// Negative ticks should be treated as 0 // Negative ticks should be treated as 0
@ -109,45 +110,45 @@ class TimeFormatterTest {
assertEquals("00:00:00", TimeFormatter.format(-1000, "hh:mm:ss")); assertEquals("00:00:00", TimeFormatter.format(-1000, "hh:mm:ss"));
assertEquals("0", TimeFormatter.format(-50, "s")); assertEquals("0", TimeFormatter.format(-50, "s"));
} }
@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"));
} }
@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"));
} }
@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"));
} }
@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"));
assertTrue(exception.getMessage().contains("Supported formats")); assertTrue(exception.getMessage().contains("Supported formats"));
} }
@Test @Test
void testFormatLargeValues() { void testFormatLargeValues() {
// 1 million ticks = 50000 seconds = 833 minutes = 13 hours 53 minutes 20 seconds // 1 million ticks = 50000 seconds = 833 minutes = 13 hours 53 minutes 20 seconds
@ -157,18 +158,18 @@ class TimeFormatterTest {
assertEquals("833", TimeFormatter.format(1000000, "m")); assertEquals("833", TimeFormatter.format(1000000, "m"));
assertEquals("13", TimeFormatter.format(1000000, "h")); assertEquals("13", TimeFormatter.format(1000000, "h"));
} }
@Test @Test
void testFormatResetTimerExample() { void testFormatResetTimerExample() {
// Typical reset timer: 6000 ticks (5 minutes) // Typical reset timer: 6000 ticks (5 minutes)
assertEquals("05:00", TimeFormatter.format(6000, "mm:ss")); assertEquals("05:00", TimeFormatter.format(6000, "mm:ss"));
// After 1 minute: 4800 ticks remaining // After 1 minute: 4800 ticks remaining
assertEquals("04:00", TimeFormatter.format(4800, "mm:ss")); assertEquals("04:00", TimeFormatter.format(4800, "mm:ss"));
// After 4 minutes 30 seconds: 600 ticks remaining // After 4 minutes 30 seconds: 600 ticks remaining
assertEquals("00:30", TimeFormatter.format(600, "mm:ss")); assertEquals("00:30", TimeFormatter.format(600, "mm:ss"));
// Just before reset: 20 ticks remaining (1 second) // Just before reset: 20 ticks remaining (1 second)
assertEquals("00:01", TimeFormatter.format(20, "mm:ss")); assertEquals("00:01", TimeFormatter.format(20, "mm:ss"));
} }

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*