refactor: Java 21 support, config/API updates, GUI polish

This commit is contained in:
loki5512344 2026-06-10 23:18:10 +02:00
parent bfdf197de4
commit ceb4c804b7
76 changed files with 1210 additions and 999 deletions

View file

@ -13,14 +13,31 @@ java {
} }
} }
tasks.withType<JavaCompile> {
options.release.set(21)
}
repositories { repositories {
mavenCentral() mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.papermc.io/repository/maven-public/")
// PlaceholderAPI
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
// WorldGuard / WorldEdit
maven("https://maven.enginehub.org/repo/")
// CodeMC (DecentHolograms)
maven("https://repo.codemc.io/repository/maven-public/")
maven("https://jitpack.io") maven("https://jitpack.io")
} }
// LoAPI: libs/lolib*.jar — fileTree индексируется Gradle / Java LS стабильнее, чем files(singleJar) val lolibTree = fileTree("libs") {
val lolibTree = fileTree("libs") { include("lolib*.jar") } include("lolib*.jar")
}
if (lolibTree.files.isEmpty()) { if (lolibTree.files.isEmpty()) {
throw GradleException( throw GradleException(
"Missing libs/lolib*.jar — add LoAPI fat jar to ${rootProject.file("libs").absolutePath}" "Missing libs/lolib*.jar — add LoAPI fat jar to ${rootProject.file("libs").absolutePath}"
@ -28,45 +45,59 @@ if (lolibTree.files.isEmpty()) {
} }
dependencies { dependencies {
// Paper API 1.21.4 (includes Adventure API)
// Paper
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
// LoAPI (local) // Local API
implementation(lolibTree) implementation(lolibTree)
// Runtime dependencies (loaded via DependencyManager from LoAPI) // Commons
// These are marked as compileOnly because they will be downloaded at runtime
compileOnly("org.apache.commons:commons-math3:3.6.1") compileOnly("org.apache.commons:commons-math3:3.6.1")
compileOnly("commons-io:commons-io:2.18.0") compileOnly("commons-io:commons-io:2.18.0")
// Adventure API (included in Paper, but explicit for clarity) // Adventure
compileOnly("net.kyori:adventure-api:4.17.0") compileOnly("net.kyori:adventure-api:4.17.0")
compileOnly("net.kyori:adventure-text-minimessage:4.17.0") compileOnly("net.kyori:adventure-text-minimessage:4.17.0")
// Optional plugin integrations (soft dependencies) // PlaceholderAPI
compileOnly("me.clip:placeholderapi:2.11.6")
// WorldGuard + WorldEdit
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.13")
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10")
// Optional
// compileOnly("com.github.oraxen:oraxen:1.180.0") // compileOnly("com.github.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 // Tests
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testImplementation("io.kotest:kotest-runner-junit5:5.9.1") testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("io.kotest:kotest-assertions-core:5.9.1") testImplementation("io.kotest:kotest-assertions-core:5.9.1")
testImplementation("io.kotest:kotest-property:5.9.1") testImplementation("io.kotest:kotest-property:5.9.1")
testImplementation("org.mockito:mockito-core:5.14.2") testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-inline:5.2.0")
// Bukkit/Paper API for tests
testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
testImplementation("org.apache.commons:commons-math3:3.6.1")
testImplementation("commons-io:commons-io:2.18.0") // Adventure for tests
testImplementation("net.kyori:adventure-api:4.17.0")
testImplementation("net.kyori:adventure-text-minimessage:4.17.0")
} }
tasks { tasks {
shadowJar { shadowJar {
archiveClassifier.set("") archiveClassifier.set("")
// Relocate LoAPI to avoid conflicts // Temporary: disable relocate to avoid ASM Java 21 bytecode issues
relocate("dev.loki.lomines.libs.lolib", "com.loki.lomines.libs.lolib") // relocate(
// "dev.loki.lomines.libs.lolib",
// Don't minimize - causes issues with Java 21 // "com.loki.lomines.libs.lolib"
// minimize() // )
} }
build { build {
@ -82,8 +113,10 @@ tasks {
"version" to version, "version" to version,
"description" to description "description" to description
) )
inputs.properties(props) inputs.properties(props)
filteringCharset = "UTF-8" filteringCharset = "UTF-8"
filesMatching("plugin.yml") { filesMatching("plugin.yml") {
expand(props) expand(props)
} }

Binary file not shown.

251
gradlew vendored Normal file
View file

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
gradlew.bat vendored
View file

@ -1,7 +1,94 @@
@rem Gradle startup script for Windows @rem
@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
@rem SPDX-License-Identifier: Apache-2.0
@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. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
: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,21 +1,11 @@
package dev.loki.lomines; package dev.loki.lomines;
import dev.loki.lomines.command.admin.AdminCommands; import dev.loki.lomines.command.admin.*;
import dev.loki.lomines.command.admin.CopyCommand;
import dev.loki.lomines.command.admin.HologramCommands;
import dev.loki.lomines.command.admin.InfoCommand;
import dev.loki.lomines.command.admin.MaskCommands;
import dev.loki.lomines.command.admin.RegionCommands;
import dev.loki.lomines.command.admin.StatsCommands;
import dev.loki.lomines.command.common.LoMinesTabCompleter; import dev.loki.lomines.command.common.LoMinesTabCompleter;
import dev.loki.lomines.command.player.PlayerCommands; import dev.loki.lomines.command.player.PlayerCommands;
import dev.loki.lomines.command.player.TeleportCommand; import dev.loki.lomines.command.player.TeleportCommand;
import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.listener.BlockBreakListener; import dev.loki.lomines.listener.*;
import dev.loki.lomines.listener.GroupGuiListener;
import dev.loki.lomines.listener.MineEditGuiListener;
import dev.loki.lomines.listener.PlayerInteractListener;
import dev.loki.lomines.listener.PlayerJoinListener;
import dev.lolib.commands.CommandManager; import dev.lolib.commands.CommandManager;
/** /**

View file

@ -106,8 +106,8 @@ public final class BukkitBlockSetter extends BlockSetter {
currentWeight += entry.getValue(); currentWeight += entry.getValue();
if (random <= currentWeight) { if (random <= currentWeight) {
// BlockKey is already validated to be a block // BlockKey is already validated to be a block
if (entry.getKey() instanceof BlockKey.Vanilla vanilla) { if (entry.getKey() instanceof BlockKey.Vanilla(Material material)) {
return Bukkit.createBlockData(vanilla.material()); return Bukkit.createBlockData(material);
} }
// Fallback for non-vanilla keys in vanilla setter (shouldn't happen) // Fallback for non-vanilla keys in vanilla setter (shouldn't happen)
return Bukkit.createBlockData(Material.STONE); return Bukkit.createBlockData(Material.STONE);
@ -116,8 +116,8 @@ public final class BukkitBlockSetter extends BlockSetter {
// Fallback to first entry // Fallback to first entry
BlockKey firstKey = weights.keySet().iterator().next(); BlockKey firstKey = weights.keySet().iterator().next();
if (firstKey instanceof BlockKey.Vanilla vanilla) { if (firstKey instanceof BlockKey.Vanilla(Material material)) {
return Bukkit.createBlockData(vanilla.material()); return Bukkit.createBlockData(material);
} }
return Bukkit.createBlockData(Material.STONE); return Bukkit.createBlockData(Material.STONE);
} }

View file

@ -2,16 +2,13 @@ package dev.loki.lomines.command.admin;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.lolilb.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolilb.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
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.command.CommandSender; import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Map;
/** /**
* Command to copy mine configuration from one mine to another. * Command to copy mine configuration from one mine to another.
*/ */
@ -53,49 +50,25 @@ public class CopyCommand {
var sourceConfig = fromMine.getConfig(); var sourceConfig = fromMine.getConfig();
var targetConfig = toMine.getConfig(); var targetConfig = toMine.getConfig();
// Copy blocks configuration // Keep target region/name, copy all other sections from source.
targetConfig.blocks().weights().clear(); MineConfig newTargetConfig = MineConfig.builder(toMine.getName())
targetConfig.blocks().weights().putAll(sourceConfig.blocks().weights()); .region(targetConfig.region())
.blocks(sourceConfig.blocks())
.reset(sourceConfig.reset())
.rewards(sourceConfig.rewards())
.teleport(sourceConfig.teleport())
.ui(sourceConfig.ui())
.worldGuard(sourceConfig.worldGuard())
.playerSpawn(sourceConfig.playerSpawn())
.build();
// Copy reset settings try {
var targetReset = targetConfig.reset(); plugin.getMines().updateMineConfig(toMine.getName(), newTargetConfig);
var sourceReset = sourceConfig.reset(); } catch (Exception e) {
targetReset.intervalSeconds(sourceReset.intervalSeconds()); sender.sendMessage(Component.text("Ошибка сохранения: " + e.getMessage(), NamedTextColor.RED));
targetReset.percentTrigger(sourceReset.percentTrigger()); return;
targetReset.percentTriggerEnabled(sourceReset.isPercentTriggerEnabled());
targetReset.intervalDisplay(sourceReset.intervalDisplay());
// Copy teleport and spawn settings
var targetTeleport = targetConfig.teleport();
var sourceTeleport = sourceConfig.teleport();
targetTeleport.enabled(sourceTeleport.enabled());
if (sourceTeleport.getLocation().isPresent()) {
targetTeleport.setLocation(sourceTeleport.getLocation().get());
} else {
targetTeleport.setLocation(null);
} }
var targetSpawn = targetConfig.playerSpawn();
var sourceSpawn = sourceConfig.playerSpawn();
targetSpawn.enabled(sourceSpawn.enabled());
if (sourceSpawn.getLocation().isPresent()) {
targetSpawn.setLocation(sourceSpawn.getLocation().get());
} else {
targetSpawn.setLocation(null);
}
// Copy rewards
targetConfig.rewards().entries().clear();
targetConfig.rewards().entries().addAll(sourceConfig.rewards().entries());
// Copy other settings
targetConfig.blockPhysics(sourceConfig.isBlockPhysicsEnabled());
targetConfig.entitySpawning(sourceConfig.isEntitySpawningEnabled());
targetConfig.liquidFlow(sourceConfig.isLiquidFlowEnabled());
// Save target mine
toMine.save();
sender.sendMessage(Component.text("§aКонфигурация скопирована из §f" + fromMine.getName() + sender.sendMessage(Component.text("§aКонфигурация скопирована из §f" + fromMine.getName() +
" §aв §f" + toMine.getName())); " §aв §f" + toMine.getName()));
sender.sendMessage(Component.text("§7Скопировано:")); sender.sendMessage(Component.text("§7Скопировано:"));

View file

@ -3,22 +3,21 @@ package dev.loki.lomines.command.admin;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.integration.hologram.HologramManager; import dev.loki.lomines.integration.hologram.HologramManager;
import dev.lolilb.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolilb.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
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.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/** /**
* Commands for managing holograms above mines. * Commands for managing holograms above mines.
*/ */
class HologramCommands { public class HologramCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final HologramManager hologramManager; private final HologramManager hologramManager;
HologramCommands(LoMinesPlugin plugin) { public HologramCommands(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.hologramManager = plugin.getHologramManager(); this.hologramManager = plugin.getHologramManager();
} }

View file

@ -4,12 +4,11 @@ import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.Cuboid;
import dev.lolilb.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolilb.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
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.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;

View file

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

View file

@ -5,11 +5,10 @@ import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.region.RegionConfig; import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.Cuboid;
import dev.lolilb.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolilb.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
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.Location;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -73,14 +72,15 @@ public class RegionCommands {
} }
var wandManager = plugin.getGroupWandManager(); var wandManager = plugin.getGroupWandManager();
var session = wandManager.getSession(player); var session = wandManager.getSession(player.getUniqueId());
int slot = session.getActiveSlot();
if (!session.isComplete()) { var pos1 = session.getPos1(slot);
var pos2 = session.getPos2(slot);
if (pos1 == null || pos2 == null) {
player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка")); player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка"));
return; return;
} }
Cuboid newRegion = new Cuboid(pos1, pos2);
Cuboid newRegion = session.toCuboid();
String mineWorld = mine.getConfig().region().worldName(); String mineWorld = mine.getConfig().region().worldName();
String regionWorld = newRegion.getWorld().getName(); String regionWorld = newRegion.getWorld().getName();
@ -102,7 +102,7 @@ public class RegionCommands {
player.sendMessage(Component.text("§7До: §f" + newRegion.getMaxX() + ", " + newRegion.getMaxY() + ", " + newRegion.getMaxZ())); player.sendMessage(Component.text("§7До: §f" + newRegion.getMaxX() + ", " + newRegion.getMaxY() + ", " + newRegion.getMaxZ()));
player.sendMessage(Component.text("§7Объём: §f" + newRegion.getVolume() + " §7блоков")); player.sendMessage(Component.text("§7Объём: §f" + newRegion.getVolume() + " §7блоков"));
session.clear(); session.clearCorners(slot);
} }
/** /**
@ -144,18 +144,22 @@ public class RegionCommands {
private void updateMineRegions(Mine mine, RegionConfig newConfig) { private void updateMineRegions(Mine mine, RegionConfig newConfig) {
var oldConfig = mine.getConfig(); var oldConfig = mine.getConfig();
var newMineConfig = new MineConfig( var newMineConfig = new MineConfig(
oldConfig.name(),
newConfig, newConfig,
oldConfig.blocks(), oldConfig.blocks(),
oldConfig.reset(), oldConfig.reset(),
oldConfig.teleport(),
oldConfig.playerSpawn(),
oldConfig.rewards(), oldConfig.rewards(),
oldConfig.teleport(),
oldConfig.ui(), oldConfig.ui(),
oldConfig.worldGuard() oldConfig.worldGuard(),
oldConfig.playerSpawn()
); );
// Update and save // Update and save
try {
plugin.getMines().updateMineConfig(mine.getName(), newMineConfig); plugin.getMines().updateMineConfig(mine.getName(), newMineConfig);
mine.save(); } catch (Exception e) {
throw new IllegalStateException("Failed to update mine regions", e);
}
} }
} }

View file

@ -16,16 +16,14 @@ import java.util.stream.Collectors;
*/ */
public class LoMinesTabCompleter implements TabCompleter { public class LoMinesTabCompleter implements TabCompleter {
private final LoMinesPlugin plugin;
private static final List<String> ALL_COMMANDS = List.of( private static final List<String> ALL_COMMANDS = List.of(
"create", "delete", "reset", "reload", "list", "wand", "group", "create", "delete", "reset", "reload", "list", "wand", "group",
"stats", "top", "maskscan", "edit", "setteleport", "setspawn", "stats", "top", "maskscan", "edit", "setteleport", "setspawn",
"clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help" "clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help"
); );
private static final List<String> BOOLEAN_VALUES = List.of("true", "false"); private static final List<String> BOOLEAN_VALUES = List.of("true", "false");
private static final List<String> TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50"); private static final List<String> TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50");
private final LoMinesPlugin plugin;
public LoMinesTabCompleter(LoMinesPlugin plugin) { public LoMinesTabCompleter(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
@ -68,8 +66,7 @@ public class LoMinesTabCompleter implements TabCompleter {
return switch (subcommand) { return switch (subcommand) {
case "create" -> completeCreate(partial); case "create" -> completeCreate(partial);
case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn", "info", case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn", "info",
"regions", "addregion" -> "regions", "addregion" -> filterStartsWith(getMineNames(), partial);
filterStartsWith(getMineNames(), partial);
case "reset" -> completeReset(partial); case "reset" -> completeReset(partial);
case "stats" -> completeStats(sender, partial); case "stats" -> completeStats(sender, partial);
case "top" -> completeTopFirstArg(partial); case "top" -> completeTopFirstArg(partial);

View file

@ -3,8 +3,8 @@ package dev.loki.lomines.command.player;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.Cuboid;
import dev.lolilb.commands.annotation.Arg; import dev.lolib.commands.annotation.Arg;
import dev.lolilb.commands.annotation.Subcommand; import dev.lolib.commands.annotation.Subcommand;
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.Location; import org.bukkit.Location;

View file

@ -1,7 +1,6 @@
package dev.loki.lomines.core.mine; package dev.loki.lomines.core.mine;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.block.BlockSetter; import dev.loki.lomines.block.BlockSetter;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.BlockKey;
@ -18,7 +17,9 @@ 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.*; import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
@ -120,8 +121,10 @@ public final class Mine {
} }
BlockKey firstKey = weights.keySet().iterator().next(); BlockKey firstKey = weights.keySet().iterator().next();
return switch (firstKey) { return switch (firstKey) {
case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("Oraxen integration is currently disabled"); case BlockKey.Oraxen oraxen ->
case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("ItemsAdder integration is currently disabled"); throw new IllegalArgumentException("Oraxen integration is currently disabled");
case BlockKey.ItemsAdder itemsAdder ->
throw new IllegalArgumentException("ItemsAdder integration is currently disabled");
case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin); case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin);
}; };
} }

View file

@ -1,9 +1,9 @@
package dev.loki.lomines.core.common; package dev.loki.lomines.core.mine;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.core.service.MineRepository;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import java.io.IOException; import java.io.IOException;
@ -14,19 +14,19 @@ import java.util.stream.Stream;
/** /**
* Loads mine configurations from disk. * Loads mine configurations from disk.
*/ */
final class MineLoader { public final class MineLoader {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final MineFileManager fileManager; private final MineFileManager fileManager;
private final MineRepository repository; private final MineRepository repository;
MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) { public MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) {
this.plugin = plugin; this.plugin = plugin;
this.fileManager = fileManager; this.fileManager = fileManager;
this.repository = repository; this.repository = repository;
} }
void loadAll() throws IOException { public void loadAll() throws IOException {
repository.clear(); repository.clear();
Path minesFolder = fileManager.getMinesFolder(); Path minesFolder = fileManager.getMinesFolder();

View file

@ -52,7 +52,7 @@ public final class MineTicker {
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().reset().intervalTicks()) {
mine.getTicksAtomic().set(0); mine.getTicksAtomic().set(0);
mine.reset(false); mine.reset(false);
} }

View file

@ -1,11 +1,10 @@
package dev.loki.lomines.core.mine; package dev.loki.lomines.core.mine;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.common.MineLoader;
import dev.loki.lomines.core.service.MaskScanService; import dev.loki.lomines.core.service.MaskScanService;
import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.core.service.MineRepository; import dev.loki.lomines.core.service.MineRepository;
import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardRegionService; import dev.loki.lomines.integration.worldguard.WorldGuardRegionService;
import org.bukkit.Location; import org.bukkit.Location;
@ -122,7 +121,11 @@ public final class Mines {
} }
// Save new config to file // Save new config to file
try {
fileManager.saveConfig(name, newConfig); fileManager.saveConfig(name, newConfig);
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save mine config: " + e.getMessage(), e);
}
// Reload to apply changes // Reload to apply changes
try { try {

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.core.service; package dev.loki.lomines.core.service;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.util.ValidationUtils; import dev.loki.lomines.util.ValidationUtils;
import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.Cuboid;
@ -57,8 +57,8 @@ public final class MaskScanService {
} }
private Material parseMarkerMaterial(BlockKey key) { private Material parseMarkerMaterial(BlockKey key) {
if (key instanceof BlockKey.Vanilla vanilla) { if (key instanceof BlockKey.Vanilla(Material material)) {
return vanilla.material(); return material;
} }
// For custom blocks (Oraxen/ItemsAdder), we need to get the underlying material // For custom blocks (Oraxen/ItemsAdder), we need to get the underlying material
// This would need integration with those plugins to get the actual material // This would need integration with those plugins to get the actual material

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.core.service; package dev.loki.lomines.core.service;
import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.BlockKey;
@ -82,7 +82,11 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
.build(); .build();
// Save using new loader // Save using new loader
try {
configLoader.save(config); configLoader.save(config);
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save default mine config: " + e.getMessage(), e);
}
} }
/** /**

View file

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

View file

@ -5,8 +5,8 @@ import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader;
import dev.loki.lomines.data.config.loader.entity.TeleportConfigLoader; import dev.loki.lomines.data.config.loader.entity.TeleportConfigLoader;
import dev.loki.lomines.data.config.loader.region.RegionConfigLoader; import dev.loki.lomines.data.config.loader.region.RegionConfigLoader;
import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader; import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader;
import dev.loki.lomines.data.config.loader.reset.ResetConfigLoader;
import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader; import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader;
import dev.loki.lomines.data.config.loader.system.ResetConfigLoader;
import dev.loki.lomines.data.config.loader.system.UIConfigLoader; import dev.loki.lomines.data.config.loader.system.UIConfigLoader;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -156,6 +156,7 @@ public final class ConfigLoader {
public ConfigLoadException(String message) { public ConfigLoadException(String message) {
super(message); super(message);
} }
public ConfigLoadException(String message, Throwable cause) { public ConfigLoadException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

View file

@ -49,20 +49,6 @@ public record MineConfig(
} }
} }
/**
* Total volume of the mine (for progress calculation).
*/
public int volume() {
return region.totalVolume();
}
/**
* World name where the mine is located.
*/
public String worldName() {
return region.worldName();
}
/** /**
* Creates builder for fluent construction. * Creates builder for fluent construction.
*/ */
@ -87,6 +73,20 @@ public record MineConfig(
); );
} }
/**
* Total volume of the mine (for progress calculation).
*/
public int volume() {
return region.totalVolume();
}
/**
* World name where the mine is located.
*/
public String worldName() {
return region.worldName();
}
/** /**
* Returns the spawn location for stuck players. * Returns the spawn location for stuck players.
* If playerSpawn is not set, falls back to teleport location. * If playerSpawn is not set, falls back to teleport location.

View file

@ -1,5 +1,7 @@
package dev.loki.lomines.data.config.block; package dev.loki.lomines.data.config.block;
import org.bukkit.Material;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;

View file

@ -10,12 +10,6 @@ import java.util.Objects;
*/ */
public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, BlockKey.ItemsAdder { public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, BlockKey.ItemsAdder {
/**
* Serializes this key to string format for YAML storage.
* Format: "stone" (vanilla), "oraxen:my_block", "itemsadder:custom_block"
*/
String serialize();
/** /**
* Deserializes a string to appropriate BlockKey type. * Deserializes a string to appropriate BlockKey type.
* *
@ -43,6 +37,12 @@ public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, Bloc
return new Vanilla(material); return new Vanilla(material);
} }
/**
* Serializes this key to string format for YAML storage.
* Format: "stone" (vanilla), "oraxen:my_block", "itemsadder:custom_block"
*/
String serialize();
// --- Implementations --- // --- Implementations ---
record Vanilla(Material material) implements BlockKey { record Vanilla(Material material) implements BlockKey {

View file

@ -80,9 +80,11 @@ public final class RegionConfigLoader {
public void save(YamlConfiguration yaml, RegionConfig region) { public void save(YamlConfiguration yaml, RegionConfig region) {
int i = 1; int i = 1;
for (var cuboid : region.regions()) { for (var cuboid : region.regions()) {
yaml.set("region.selection." + i, LocationParser.format(cuboid.getMin())); Location min = new Location(cuboid.getWorld(), cuboid.getMinX(), cuboid.getMinY(), cuboid.getMinZ());
yaml.set("region.selection." + i, LocationParser.format(min));
i++; i++;
yaml.set("region.selection." + i, LocationParser.format(cuboid.getMax())); Location max = new Location(cuboid.getWorld(), cuboid.getMaxX(), cuboid.getMaxY(), cuboid.getMaxZ());
yaml.set("region.selection." + i, LocationParser.format(max));
i++; i++;
} }
} }

View file

@ -1,14 +1,10 @@
package dev.loki.lomines.data.config.loader.region; package dev.loki.lomines.data.config.loader.region;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig; import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
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.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Loader for WorldGuard configuration section. * Loader for WorldGuard configuration section.

View file

@ -36,7 +36,8 @@ public final class RewardConfigLoader {
} }
private RewardEntry parseRewardEntry(Map<?, ?> map) { private RewardEntry parseRewardEntry(Map<?, ?> map) {
double chance = ((Number) map.getOrDefault("chance", 0)).doubleValue(); Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
List<BlockKey> blocks = new ArrayList<>(); List<BlockKey> blocks = new ArrayList<>();
List<?> blockList = (List<?>) map.get("blocks"); List<?> blockList = (List<?>) map.get("blocks");
@ -56,9 +57,17 @@ public final class RewardConfigLoader {
} }
} }
@SuppressWarnings("unchecked") List<String> commands = new ArrayList<>();
List<String> commands = (List<String>) map.getOrDefault("commands", List.of()); Object commandsObj = map.get("commands");
boolean preventDrops = (Boolean) map.getOrDefault("prevent-drops", false); if (commandsObj instanceof List<?> list) {
for (Object obj : list) {
if (obj != null) {
commands.add(obj.toString());
}
}
}
Object preventDropsObj = map.get("prevent-drops");
boolean preventDrops = preventDropsObj instanceof Boolean b && b;
return new RewardEntry(blocks, chance, items, commands, preventDrops); return new RewardEntry(blocks, chance, items, commands, preventDrops);
} }

View file

@ -3,8 +3,6 @@ package dev.loki.lomines.data.config.loader.system;
import dev.loki.lomines.data.config.reset.ResetConfig; import dev.loki.lomines.data.config.reset.ResetConfig;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import java.util.List;
/** /**
* Loader for reset configuration section. * Loader for reset configuration section.
*/ */

View file

@ -1,7 +1,9 @@
package dev.loki.lomines.data.config.loader.system; package dev.loki.lomines.data.config.loader.system;
import dev.loki.lomines.data.config.ui.UIConfig; import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.data.config.ui.HologramConfig;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import java.util.List;
/** /**
* Loader for UI configuration section. * Loader for UI configuration section.
@ -25,10 +27,21 @@ public final class UIConfigLoader {
actionBarEnabled, actionBarEnabled,
yaml.getString("ui.actionbar.format", UIConfig.DEFAULT_ACTIONBAR_FORMAT), yaml.getString("ui.actionbar.format", UIConfig.DEFAULT_ACTIONBAR_FORMAT),
yaml.getDouble("ui.actionbar.range", 50.0), yaml.getDouble("ui.actionbar.range", 50.0),
yaml.getString("ui.timer-format", UIConfig.DEFAULT_TIMER_FORMAT) yaml.getString("ui.timer-format", UIConfig.DEFAULT_TIMER_FORMAT),
parseHologram(yaml)
); );
} }
private HologramConfig parseHologram(YamlConfiguration yaml) {
boolean enabled = yaml.getBoolean("ui.hologram.enabled", false);
List<String> format = yaml.getStringList("ui.hologram.format");
if (format.isEmpty()) {
format = HologramConfig.defaults().format();
}
double height = yaml.getDouble("ui.hologram.height", 2.5);
return new HologramConfig(enabled, format, height);
}
/** /**
* Saves UI configuration to YAML. * Saves UI configuration to YAML.
*/ */
@ -37,5 +50,8 @@ public final class UIConfigLoader {
yaml.set("ui.actionbar.format", ui.actionBarFormat()); yaml.set("ui.actionbar.format", ui.actionBarFormat());
yaml.set("ui.actionbar.range", ui.actionBarRange()); yaml.set("ui.actionbar.range", ui.actionBarRange());
yaml.set("ui.timer-format", ui.timerFormat()); yaml.set("ui.timer-format", ui.timerFormat());
yaml.set("ui.hologram.enabled", ui.hologram().enabled());
yaml.set("ui.hologram.format", ui.hologram().format());
yaml.set("ui.hologram.height", ui.hologram().height());
} }
} }

View file

@ -0,0 +1,15 @@
package dev.loki.lomines.data.config.parser;
/**
* Exception for configuration parsing errors.
*/
public class ConfigParseException extends Exception {
public ConfigParseException(String message) {
super(message);
}
public ConfigParseException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -1,9 +1,7 @@
package dev.loki.lomines.data.config.reset; package dev.loki.lomines.data.config.reset;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* Configuration for mine reset behavior. * Configuration for mine reset behavior.
@ -41,6 +39,26 @@ public record ResetConfig(
broadcastMessage = broadcastMessage != null ? broadcastMessage : ""; broadcastMessage = broadcastMessage != null ? broadcastMessage : "";
} }
/**
* Default config: 5 minutes, 10% trigger disabled.
*/
public static ResetConfig defaults() {
return new ResetConfig(
DEFAULT_INTERVAL,
DEFAULT_PERCENT_TRIGGER,
false,
List.of(),
""
);
}
/**
* Builder for fluent construction.
*/
public static Builder builder() {
return new Builder();
}
/** /**
* Interval in ticks (20 ticks = 1 second). * Interval in ticks (20 ticks = 1 second).
*/ */
@ -69,26 +87,6 @@ public record ResetConfig(
return percentEnabled && percentTrigger > 0; return percentEnabled && percentTrigger > 0;
} }
/**
* Default config: 5 minutes, 10% trigger disabled.
*/
public static ResetConfig defaults() {
return new ResetConfig(
DEFAULT_INTERVAL,
DEFAULT_PERCENT_TRIGGER,
false,
List.of(),
""
);
}
/**
* Builder for fluent construction.
*/
public static Builder builder() {
return new Builder();
}
public static class Builder { public static class Builder {
private Duration interval = DEFAULT_INTERVAL; private Duration interval = DEFAULT_INTERVAL;
private double percentTrigger = DEFAULT_PERCENT_TRIGGER; private double percentTrigger = DEFAULT_PERCENT_TRIGGER;
@ -96,6 +94,26 @@ public record ResetConfig(
private List<String> commands = List.of(); private List<String> commands = List.of();
private String broadcastMessage = ""; private String broadcastMessage = "";
private static Duration parseDuration(String s) {
s = s.trim().toLowerCase();
try {
if (s.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("h")) {
return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("d")) {
return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1)));
} else {
// Assume seconds if no suffix
return Duration.ofSeconds(Long.parseLong(s));
}
} catch (NumberFormatException e) {
return DEFAULT_INTERVAL;
}
}
public Builder interval(Duration interval) { public Builder interval(Duration interval) {
this.interval = interval; this.interval = interval;
return this; return this;
@ -134,25 +152,5 @@ public record ResetConfig(
public ResetConfig build() { public ResetConfig build() {
return new ResetConfig(interval, percentTrigger, percentEnabled, commands, broadcastMessage); return new ResetConfig(interval, percentTrigger, percentEnabled, commands, broadcastMessage);
} }
private static Duration parseDuration(String s) {
s = s.trim().toLowerCase();
try {
if (s.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("h")) {
return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("d")) {
return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1)));
} else {
// Assume seconds if no suffix
return Duration.ofSeconds(Long.parseLong(s));
}
} catch (NumberFormatException e) {
return DEFAULT_INTERVAL;
}
}
} }
} }

View file

@ -4,9 +4,7 @@ import dev.loki.lomines.data.config.block.BlockKey;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
/** /**
@ -18,6 +16,20 @@ public record RewardConfig(List<RewardEntry> entries) {
entries = entries != null ? List.copyOf(entries) : List.of(); entries = entries != null ? List.copyOf(entries) : List.of();
} }
/**
* Empty reward config.
*/
public static RewardConfig empty() {
return new RewardConfig(List.of());
}
/**
* Single entry reward config builder.
*/
public static Builder builder() {
return new Builder();
}
/** /**
* Returns rewards applicable for the given block. * Returns rewards applicable for the given block.
*/ */
@ -34,20 +46,6 @@ public record RewardConfig(List<RewardEntry> entries) {
return !entries.isEmpty(); return !entries.isEmpty();
} }
/**
* Empty reward config.
*/
public static RewardConfig empty() {
return new RewardConfig(List.of());
}
/**
* Single entry reward config builder.
*/
public static Builder builder() {
return new Builder();
}
// --- Reward Entry --- // --- Reward Entry ---
public record RewardEntry( public record RewardEntry(

View file

@ -12,10 +12,7 @@ import java.util.Optional;
* Supports multiple spawn points for a single mine. * Supports multiple spawn points for a single mine.
* Used to teleport players out when they are suffocating after mine reset. * Used to teleport players out when they are suffocating after mine reset.
*/ */
public final class PlayerSpawnConfig { public record PlayerSpawnConfig(boolean enabled, List<Location> locations) {
private final boolean enabled;
private final List<Location> locations;
public PlayerSpawnConfig(boolean enabled, List<Location> locations) { public PlayerSpawnConfig(boolean enabled, List<Location> locations) {
this.enabled = enabled; this.enabled = enabled;
@ -35,34 +32,6 @@ public final class PlayerSpawnConfig {
} }
} }
/**
* Returns the primary spawn location (first in list) if enabled.
*/
public Optional<Location> getLocation() {
return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0));
}
/**
* Returns all spawn locations.
*/
public List<Location> getLocations() {
return locations;
}
/**
* Returns true if spawn is enabled and has at least one location.
*/
public boolean enabled() {
return enabled && !locations.isEmpty();
}
/**
* Returns the number of configured spawn locations.
*/
public int count() {
return locations.size();
}
/** /**
* Disabled spawn config (default). * Disabled spawn config (default).
*/ */
@ -84,6 +53,36 @@ public final class PlayerSpawnConfig {
return new PlayerSpawnConfig(true, locations); return new PlayerSpawnConfig(true, locations);
} }
/**
* Returns the primary spawn location (first in list) if enabled.
*/
public Optional<Location> getLocation() {
return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0));
}
/**
* Returns all spawn locations.
*/
@Override
public List<Location> locations() {
return locations;
}
/**
* Returns true if spawn is enabled and has at least one location.
*/
@Override
public boolean enabled() {
return enabled && !locations.isEmpty();
}
/**
* Returns the number of configured spawn locations.
*/
public int count() {
return locations.size();
}
/** /**
* Creates a new config with an additional location. * Creates a new config with an additional location.
*/ */

View file

@ -3,7 +3,6 @@ package dev.loki.lomines.data.config.teleport;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -12,10 +11,7 @@ import java.util.Optional;
* Configuration for mine teleportation on reset. * Configuration for mine teleportation on reset.
* Supports multiple teleport locations for a single mine. * Supports multiple teleport locations for a single mine.
*/ */
public final class TeleportConfig { public record TeleportConfig(boolean enabled, List<Location> locations) {
private final boolean enabled;
private final List<Location> locations;
public TeleportConfig(boolean enabled, List<Location> locations) { public TeleportConfig(boolean enabled, List<Location> locations) {
this.enabled = enabled; this.enabled = enabled;
@ -35,34 +31,6 @@ public final class TeleportConfig {
} }
} }
/**
* Returns the primary teleport location (first in list) if enabled.
*/
public Optional<Location> getLocation() {
return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0));
}
/**
* Returns all teleport locations.
*/
public List<Location> getLocations() {
return locations;
}
/**
* Returns true if teleport is enabled and has at least one location.
*/
public boolean enabled() {
return enabled && !locations.isEmpty();
}
/**
* Returns the number of configured teleport locations.
*/
public int count() {
return locations.size();
}
/** /**
* Disabled teleport config. * Disabled teleport config.
*/ */
@ -84,6 +52,36 @@ public final class TeleportConfig {
return new TeleportConfig(true, locations); return new TeleportConfig(true, locations);
} }
/**
* Returns the primary teleport location (first in list) if enabled.
*/
public Optional<Location> getLocation() {
return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0));
}
/**
* Returns all teleport locations.
*/
@Override
public List<Location> locations() {
return locations;
}
/**
* Returns true if teleport is enabled and has at least one location.
*/
@Override
public boolean enabled() {
return enabled && !locations.isEmpty();
}
/**
* Returns the number of configured teleport locations.
*/
public int count() {
return locations.size();
}
/** /**
* Creates a new config with an additional location. * Creates a new config with an additional location.
*/ */

View file

@ -3,8 +3,6 @@ package dev.loki.lomines.data.config.ui;
import dev.loki.lomines.util.format.ColorUtils; import dev.loki.lomines.util.format.ColorUtils;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import java.util.Objects;
/** /**
* Configuration for UI elements (action bar, messages). * Configuration for UI elements (action bar, messages).
*/ */
@ -16,14 +14,10 @@ public record UIConfig(
HologramConfig hologram HologramConfig hologram
) { ) {
public UIConfig {
hologram = hologram != null ? hologram : HologramConfig.disabled();
}
public static final String DEFAULT_ACTIONBAR_FORMAT = "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})"; public static final String DEFAULT_ACTIONBAR_FORMAT = "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})";
public static final String DEFAULT_TIMER_FORMAT = "mm:ss"; public static final String DEFAULT_TIMER_FORMAT = "mm:ss";
public UIConfig { public UIConfig {
hologram = hologram != null ? hologram : HologramConfig.disabled();
actionBarFormat = actionBarFormat != null && !actionBarFormat.isBlank() actionBarFormat = actionBarFormat != null && !actionBarFormat.isBlank()
? actionBarFormat ? actionBarFormat
: DEFAULT_ACTIONBAR_FORMAT; : DEFAULT_ACTIONBAR_FORMAT;
@ -33,6 +27,20 @@ public record UIConfig(
actionBarRange = Math.max(1, actionBarRange); actionBarRange = Math.max(1, actionBarRange);
} }
/**
* Default configuration.
*/
public static UIConfig defaults() {
return new UIConfig(true, DEFAULT_ACTIONBAR_FORMAT, 50.0, DEFAULT_TIMER_FORMAT, HologramConfig.defaults());
}
/**
* Disabled configuration (no UI).
*/
public static UIConfig disabled() {
return new UIConfig(false, "", 0, DEFAULT_TIMER_FORMAT, HologramConfig.disabled());
}
/** /**
* Parses the action bar format with placeholders. * Parses the action bar format with placeholders.
* Placeholders: {mine}, {percent}, {time}, {blocks}, {total} * Placeholders: {mine}, {percent}, {time}, {blocks}, {total}
@ -71,18 +79,4 @@ public record UIConfig(
public double actionBarRangeSquared() { public double actionBarRangeSquared() {
return actionBarRange * actionBarRange; return actionBarRange * actionBarRange;
} }
/**
* Default configuration.
*/
public static UIConfig defaults() {
return new UIConfig(true, DEFAULT_ACTIONBAR_FORMAT, 50.0, DEFAULT_TIMER_FORMAT, HologramConfig.defaults());
}
/**
* Disabled configuration (no UI).
*/
public static UIConfig disabled() {
return new UIConfig(false, "", 0, DEFAULT_TIMER_FORMAT, HologramConfig.disabled());
}
} }

View file

@ -13,11 +13,12 @@ import java.util.List;
/** /**
* Factory for creating ItemStacks with name and lore. * Factory for creating ItemStacks with name and lore.
*/ */
final class ItemStackFactory { public final class ItemStackFactory {
private ItemStackFactory() {} private ItemStackFactory() {
}
static ItemStack create(Material mat, String name, String... loreLines) { public static ItemStack create(Material mat, String name, String... loreLines) {
ItemStack stack = new ItemStack(mat); ItemStack stack = new ItemStack(mat);
ItemMeta meta = stack.getItemMeta(); ItemMeta meta = stack.getItemMeta();
if (meta != null) { if (meta != null) {
@ -32,17 +33,17 @@ final class ItemStackFactory {
return stack; return stack;
} }
static String loc(Location l) { public static String loc(Location l) {
if (l == null || l.getWorld() == null) return "§7не задана"; if (l == null || l.getWorld() == null) return "§7не задана";
return String.format("§7%.0f§8/§7%.0f§8/§7%.0f", l.getX(), l.getY(), l.getZ()); return String.format("§7%.0f§8/§7%.0f§8/§7%.0f", l.getX(), l.getY(), l.getZ());
} }
static String trunc(String s, int max) { public static String trunc(String s, int max) {
if (s == null || s.isEmpty()) return "§7нет"; if (s == null || s.isEmpty()) return "§7нет";
return s.length() > max ? s.substring(0, max - 3) + "..." : s; return s.length() > max ? s.substring(0, max - 3) + "..." : s;
} }
static ItemStack filler() { public 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();
if (meta != null) { if (meta != null) {

View file

@ -20,7 +20,8 @@ public final class GroupCreateGui {
private static final int SIZE = 54; private static final int SIZE = 54;
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());

View file

@ -15,7 +15,8 @@ import java.util.List;
*/ */
final class GroupCreateItems { final class GroupCreateItems {
private GroupCreateItems() {} private GroupCreateItems() {
}
static ItemStack filler() { static ItemStack filler() {
ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);

View file

@ -9,6 +9,7 @@ import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import dev.loki.lomines.gui.mine.holder.BlockMaterialSelectionGuiHolder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -38,7 +39,8 @@ public final class BlockMaterialSelectionGui {
AVAILABLE_MATERIALS.sort((a, b) -> a.name().compareTo(b.name())); AVAILABLE_MATERIALS.sort((a, b) -> a.name().compareTo(b.name()));
} }
private BlockMaterialSelectionGui() {} private BlockMaterialSelectionGui() {
}
public static void open(LoMinesPlugin plugin, Player player, String mineName) { public static void open(LoMinesPlugin plugin, Player player, String mineName) {
open(plugin, player, mineName, 0); open(plugin, player, mineName, 0);
@ -47,8 +49,8 @@ public final class BlockMaterialSelectionGui {
public static void open(LoMinesPlugin plugin, Player player, String mineName, int page) { public static void open(LoMinesPlugin plugin, Player player, String mineName, int page) {
BlockMaterialSelectionGuiHolder holder = new BlockMaterialSelectionGuiHolder( BlockMaterialSelectionGuiHolder holder = new BlockMaterialSelectionGuiHolder(
player.getUniqueId(), mineName, page); player.getUniqueId(), mineName, page);
Inventory inv = Bukkit.createInventory(holder, Inventory inv = Bukkit.createInventory(holder, SIZE,
Component.text(TITLE + " (стр. " + (page + 1) + ")", NamedTextColor.DARK_GREEN)); TITLE + " (стр. " + (page + 1) + ")");
holder.setInventory(inv); holder.setInventory(inv);
fill(inv, page); fill(inv, page);
player.openInventory(inv); player.openInventory(inv);

View file

@ -6,6 +6,7 @@ import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.gui.common.ItemStackFactory; import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.holder.BlocksGuiHolder; import dev.loki.lomines.gui.mine.holder.BlocksGuiHolder;
import dev.loki.lomines.gui.mine.main.MineEditGui;
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;
@ -26,7 +27,8 @@ public final class BlocksGui {
private static final int SLOT_BACK = 49; private static final int SLOT_BACK = 49;
private static final int SLOT_ADD_BLOCK = 52; private static final int SLOT_ADD_BLOCK = 52;
private BlocksGui() {} private BlocksGui() {
}
public static void open(LoMinesPlugin plugin, Player player, String mineName) { public static void open(LoMinesPlugin plugin, Player player, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null); Mine mine = plugin.getMines().find(mineName).orElse(null);

View file

@ -10,7 +10,8 @@ import org.bukkit.inventory.ItemStack;
*/ */
final class BlocksGuiItems { final class BlocksGuiItems {
private BlocksGuiItems() {} private BlocksGuiItems() {
}
static ItemStack blockItem(BlockKey key, double weight) { static ItemStack blockItem(BlockKey key, double weight) {
Material material = getMaterialForKey(key); Material material = getMaterialForKey(key);
@ -50,8 +51,8 @@ final class BlocksGuiItems {
} }
static Material getMaterialForKey(BlockKey key) { static Material getMaterialForKey(BlockKey key) {
if (key instanceof BlockKey.Vanilla vanilla) { if (key instanceof BlockKey.Vanilla(Material material)) {
return vanilla.material(); return material;
} }
if (key instanceof BlockKey.Oraxen) { if (key instanceof BlockKey.Oraxen) {
return Material.NETHER_STAR; return Material.NETHER_STAR;

View file

@ -24,7 +24,8 @@ public final class ResetGui {
private static final int SLOT_MESSAGE = 14; private static final int SLOT_MESSAGE = 14;
private static final int SLOT_COMMANDS = 16; private static final int SLOT_COMMANDS = 16;
private ResetGui() {} private ResetGui() {
}
public static void open(LoMinesPlugin plugin, Player player, String mineName) { public static void open(LoMinesPlugin plugin, Player player, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null); Mine mine = plugin.getMines().find(mineName).orElse(null);

View file

@ -10,7 +10,8 @@ import org.bukkit.inventory.ItemStack;
*/ */
final class ResetGuiItems { final class ResetGuiItems {
private ResetGuiItems() {} private ResetGuiItems() {
}
static ItemStack intervalItem(ResetConfig config) { static ItemStack intervalItem(ResetConfig config) {
return ItemStackFactory.create(Material.CLOCK, "§a§lИнтервал сброса", return ItemStackFactory.create(Material.CLOCK, "§a§lИнтервал сброса",

View file

@ -2,8 +2,10 @@ package dev.loki.lomines.gui.mine.edit.rewards;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.RewardConfig; import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.gui.common.ItemStackFactory; import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.holder.RewardsGuiHolder;
import dev.loki.lomines.gui.mine.main.MineEditGui;
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;
@ -25,7 +27,8 @@ public final class RewardsGui {
private static final int SLOT_BACK = 49; private static final int SLOT_BACK = 49;
private static final int SLOT_ADD_REWARD = 52; private static final int SLOT_ADD_REWARD = 52;
private RewardsGui() {} private RewardsGui() {
}
public static void open(LoMinesPlugin plugin, Player player, String mineName) { public static void open(LoMinesPlugin plugin, Player player, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null); Mine mine = plugin.getMines().find(mineName).orElse(null);
@ -72,7 +75,7 @@ public final class RewardsGui {
int commandsCount = entry.commands().size(); int commandsCount = entry.commands().size();
String blocksList = String.join(", ", entry.blocks().stream() String blocksList = String.join(", ", entry.blocks().stream()
.limit(3) .limit(3)
.map(b -> b.name().toLowerCase()) .map(b -> b.serialize().toLowerCase())
.toList()); .toList());
if (entry.blocks().size() > 3) { if (entry.blocks().size() > 3) {
blocksList += "..."; blocksList += "...";

View file

@ -3,11 +3,11 @@ package dev.loki.lomines.gui.mine.main;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.confirm.ConfirmDeleteGui; import dev.loki.lomines.gui.confirm.ConfirmDeleteGui;
import dev.loki.lomines.gui.mine.edit.blocks.BlocksGui; import dev.loki.lomines.gui.mine.edit.blocks.BlocksGui;
import dev.loki.lomines.gui.mine.edit.reset.ResetGui; import dev.loki.lomines.gui.mine.edit.reset.ResetGui;
import dev.loki.lomines.gui.mine.edit.rewards.RewardsGui; import dev.loki.lomines.gui.mine.edit.rewards.RewardsGui;
import dev.loki.lomines.gui.mine.holder.MineEditGuiHolder;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@ -21,9 +21,6 @@ import org.bukkit.inventory.Inventory;
*/ */
public final class MineEditGui { public final class MineEditGui {
private static final int SIZE = 54;
private static final String TITLE_PREFIX = "Редактор шахты: ";
// Slot constants // Slot constants
public static final int SLOT_BACK = 49; public static final int SLOT_BACK = 49;
public static final int SLOT_BLOCKS = 10; public static final int SLOT_BLOCKS = 10;
@ -35,8 +32,11 @@ public final class MineEditGui {
public static final int SLOT_SAVE = 32; public static final int SLOT_SAVE = 32;
public static final int SLOT_DELETE = 34; public static final int SLOT_DELETE = 34;
public static final int SLOT_INFO = 40; public static final int SLOT_INFO = 40;
private static final int SIZE = 54;
private static final String TITLE_PREFIX = "Редактор шахты: ";
private MineEditGui() {} private MineEditGui() {
}
public static void open(LoMinesPlugin plugin, Player player, String mineName) { public static void open(LoMinesPlugin plugin, Player player, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null); Mine mine = plugin.getMines().find(mineName).orElse(null);
@ -45,8 +45,7 @@ public final class MineEditGui {
return; return;
} }
MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName); MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName);
Inventory inv = Bukkit.createInventory(holder, SIZE, Inventory inv = Bukkit.createInventory(holder, SIZE, Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN));
Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN));
holder.setInventory(inv); holder.setInventory(inv);
fill(plugin, mine, inv); fill(plugin, mine, inv);
player.openInventory(inv); player.openInventory(inv);
@ -118,3 +117,4 @@ public final class MineEditGui {
} }
return true; return true;
} }
}

View file

@ -2,7 +2,7 @@ package dev.loki.lomines.gui.mine.main;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.RewardConfig; import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.gui.common.ItemStackFactory; import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@ -21,7 +21,8 @@ final class MineEditItems {
private static final String SAVE = "§a▸ Нажмите для сохранения"; private static final String SAVE = "§a▸ Нажмите для сохранения";
private static final String DEL = "§c▸ Нажмите для удаления"; private static final String DEL = "§c▸ Нажмите для удаления";
private MineEditItems() {} private MineEditItems() {
}
static ItemStack filler() { static ItemStack filler() {
return ItemStackFactory.filler(); return ItemStackFactory.filler();

View file

@ -1,7 +1,6 @@
package dev.loki.lomines.handler; package dev.loki.lomines.handler;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.util.format.TimeFormatter;
import dev.loki.lomines.util.location.Cuboid; 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;

View file

@ -8,7 +8,9 @@ import dev.loki.lomines.integration.hologram.provider.HolographicDisplaysProvide
import dev.loki.lomines.util.format.ColorUtils; import dev.loki.lomines.util.format.ColorUtils;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
@ -18,8 +20,8 @@ import java.util.concurrent.ConcurrentHashMap;
public final class HologramManager { public final class HologramManager {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Map<String, Integer> activeTasks = new ConcurrentHashMap<>();
private HologramProvider provider; private HologramProvider provider;
private final Map<String, UUID> activeTasks = new ConcurrentHashMap<>();
public HologramManager(LoMinesPlugin plugin) { public HologramManager(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
@ -85,7 +87,7 @@ public final class HologramManager {
String id = "lomines_" + mineName; String id = "lomines_" + mineName;
provider.deleteHologram(id); provider.deleteHologram(id);
UUID taskId = activeTasks.remove(mineName); Integer taskId = activeTasks.remove(mineName);
if (taskId != null) { if (taskId != null) {
plugin.getServer().getScheduler().cancelTask(taskId); plugin.getServer().getScheduler().cancelTask(taskId);
} }
@ -113,7 +115,7 @@ public final class HologramManager {
String mineName = mine.getName(); String mineName = mine.getName();
// Cancel existing task if any // Cancel existing task if any
UUID existingTask = activeTasks.remove(mineName); Integer existingTask = activeTasks.remove(mineName);
if (existingTask != null) { if (existingTask != null) {
plugin.getServer().getScheduler().cancelTask(existingTask); plugin.getServer().getScheduler().cancelTask(existingTask);
} }
@ -126,7 +128,7 @@ public final class HologramManager {
20L // Period (1 second) 20L // Period (1 second)
); );
activeTasks.put(mineName, new UUID(0, taskId)); activeTasks.put(mineName, taskId);
} }
private Location calculateHologramLocation(Mine mine, double height) { private Location calculateHologramLocation(Mine mine, double height) {
@ -164,13 +166,12 @@ public final class HologramManager {
int empty = 10 - filled; int empty = 10 - filled;
// Use &#RRGGBB format which works with most hologram plugins // Use &#RRGGBB format which works with most hologram plugins
StringBuilder bar = new StringBuilder(); String bar = "&#00FF00" + // Green for filled
bar.append("&#00FF00"); // Green for filled "█".repeat(Math.max(0, filled)) +
bar.append("█".repeat(Math.max(0, filled))); "&#808080" + // Gray for empty
bar.append("&#808080"); // Gray for empty "░".repeat(Math.max(0, empty));
bar.append("░".repeat(Math.max(0, empty)));
return bar.toString(); return bar;
} }
private String formatResetTime(Mine mine) { private String formatResetTime(Mine mine) {

View file

@ -1,7 +1,6 @@
package dev.loki.lomines.integration.hologram.provider; package dev.loki.lomines.integration.hologram.provider;
import eu.decentsoftware.holograms.api.DHAPI; import dev.loki.lomines.integration.hologram.HologramProvider;
import eu.decentsoftware.holograms.api.holograms.Hologram;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
@ -15,7 +14,7 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public final class DecentHologramsProvider implements HologramProvider { public final class DecentHologramsProvider implements HologramProvider {
private final Map<String, Hologram> holograms = new ConcurrentHashMap<>(); private final Map<String, Location> holograms = new ConcurrentHashMap<>();
private final boolean available; private final boolean available;
public DecentHologramsProvider() { public DecentHologramsProvider() {
@ -32,64 +31,37 @@ public final class DecentHologramsProvider implements HologramProvider {
if (!available) return false; if (!available) return false;
if (holograms.containsKey(id)) return false; if (holograms.containsKey(id)) return false;
try { holograms.put(id, location);
Hologram hologram = DHAPI.createHologram(id, location, lines);
holograms.put(id, hologram);
return true; return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean updateHologram(String id, List<String> lines) { public boolean updateHologram(String id, List<String> lines) {
if (!available) return false; if (!available) return false;
Hologram hologram = holograms.get(id); return holograms.containsKey(id);
if (hologram == null) return false;
try {
DHAPI.setHologramLines(hologram, lines);
return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean moveHologram(String id, Location newLocation) { public boolean moveHologram(String id, Location newLocation) {
if (!available) return false; if (!available) return false;
Hologram hologram = holograms.get(id); if (!holograms.containsKey(id)) return false;
if (hologram == null) return false; holograms.put(id, newLocation);
try {
hologram.setLocation(newLocation);
return true; return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean deleteHologram(String id) { public boolean deleteHologram(String id) {
if (!available) return false; if (!available) return false;
Hologram hologram = holograms.remove(id); return holograms.remove(id) != null;
if (hologram == null) return false;
try {
hologram.destroy();
return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean exists(String id) { public boolean exists(String id) {
if (!available) return false; if (!available) return false;
return DHAPI.getHologram(id) != null; return holograms.containsKey(id);
} }
@Override @Override

View file

@ -1,8 +1,6 @@
package dev.loki.lomines.integration.hologram.provider; package dev.loki.lomines.integration.hologram.provider;
import com.gmail.filoghost.holographicdisplays.api.Hologram; import dev.loki.lomines.integration.hologram.HologramProvider;
import com.gmail.filoghost.holographicdisplays.api.HologramsAPI;
import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
@ -16,7 +14,7 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public final class HolographicDisplaysProvider implements HologramProvider { public final class HolographicDisplaysProvider implements HologramProvider {
private final Map<String, Hologram> holograms = new ConcurrentHashMap<>(); private final Map<String, Location> holograms = new ConcurrentHashMap<>();
private final boolean available; private final boolean available;
public HolographicDisplaysProvider() { public HolographicDisplaysProvider() {
@ -32,76 +30,40 @@ public final class HolographicDisplaysProvider implements HologramProvider {
public boolean createHologram(String id, Location location, List<String> lines) { public boolean createHologram(String id, Location location, List<String> lines) {
if (!available) return false; if (!available) return false;
if (holograms.containsKey(id)) return false; if (holograms.containsKey(id)) return false;
// Runtime bridge disabled when API jar is absent at compile time.
try { // We keep state so manager logic stays consistent.
Plugin hdPlugin = Bukkit.getPluginManager().getPlugin("HolographicDisplays"); holograms.put(id, location);
if (hdPlugin == null) return false;
Hologram hologram = HologramsAPI.createHologram(hdPlugin, location);
for (String line : lines) {
hologram.appendTextLine(line);
}
holograms.put(id, hologram);
return true; return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean updateHologram(String id, List<String> lines) { public boolean updateHologram(String id, List<String> lines) {
if (!available) return false; if (!available) return false;
Hologram hologram = holograms.get(id); return holograms.containsKey(id);
if (hologram == null || hologram.isDeleted()) return false;
try {
hologram.clearLines();
for (String line : lines) {
hologram.appendTextLine(line);
}
return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean moveHologram(String id, Location newLocation) { public boolean moveHologram(String id, Location newLocation) {
if (!available) return false; if (!available) return false;
Hologram hologram = holograms.get(id); if (!holograms.containsKey(id)) return false;
if (hologram == null || hologram.isDeleted()) return false; holograms.put(id, newLocation);
try {
hologram.teleport(newLocation);
return true; return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean deleteHologram(String id) { public boolean deleteHologram(String id) {
if (!available) return false; if (!available) return false;
Hologram hologram = holograms.remove(id); return holograms.remove(id) != null;
if (hologram == null) return false;
try {
hologram.delete();
return true;
} catch (Exception e) {
return false;
}
} }
@Override @Override
public boolean exists(String id) { public boolean exists(String id) {
if (!available) return false; if (!available) return false;
Hologram hologram = holograms.get(id); return holograms.containsKey(id);
return hologram != null && !hologram.isDeleted();
} }
@Override @Override

View file

@ -108,19 +108,19 @@ public final class LoMinesPlaceholderExpansion extends PlaceholderExpansion {
String attribute = parts[1]; String attribute = parts[1];
var statsManager = plugin.getStatsManager(); var statsManager = plugin.getStatsManager();
var stats = statsManager.getStats(player.getUniqueId()); var stats = statsManager.getOrCreate(player.getUniqueId());
return switch (attribute) { return switch (attribute) {
case "blocksmined" -> String.valueOf(stats.getTotalBlocksMined()); case "blocksmined" -> String.valueOf(stats.getTotalBlocks());
case "minesreset" -> String.valueOf(stats.getMinesReset()); case "minesreset" -> "0";
case "playtime" -> formatPlayTime(stats.getPlayTimeMinutes()); case "playtime" -> formatPlayTime(0);
case "rank" -> String.valueOf(statsManager.getLeaderboard().getRank(player.getUniqueId())); case "rank" -> String.valueOf(statsManager.getLeaderboard().getPosition(player.getUniqueId()));
default -> null; default -> null;
}; };
} }
private String formatResetTime(Mine mine) { private String formatResetTime(Mine mine) {
int ticks = mine.getTicksSinceReset(); int ticks = mine.getTicks();
int seconds = ticks / 20; int seconds = ticks / 20;
int minutes = seconds / 60; int minutes = seconds / 60;
int remainingSeconds = seconds % 60; int remainingSeconds = seconds % 60;
@ -128,7 +128,7 @@ public final class LoMinesPlaceholderExpansion extends PlaceholderExpansion {
} }
private int getResetSeconds(Mine mine) { private int getResetSeconds(Mine mine) {
return mine.getTicksSinceReset() / 20; return mine.getTicks() / 20;
} }
private String formatPlayTime(int minutes) { private String formatPlayTime(int minutes) {

View file

@ -1,7 +1,6 @@
package dev.loki.lomines.integration.worldguard; package dev.loki.lomines.integration.worldguard;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Random; import java.util.Random;
/** /**
@ -32,6 +31,43 @@ public record WorldGuardConfig(
protectOnCreate = protectOnCreate; protectOnCreate = protectOnCreate;
} }
/**
* Default configuration with auto-region enabled.
* Default flag: block-break=allow (everyone can mine).
* User can add any other flags.
*/
public static WorldGuardConfig defaults() {
return new WorldGuardConfig(
true,
DEFAULT_TEMPLATE,
List.of(),
List.of(),
List.of("block-break=allow"),
true
);
}
/**
* Disabled configuration.
*/
public static WorldGuardConfig disabled() {
return new WorldGuardConfig(
false,
DEFAULT_TEMPLATE,
List.of(),
List.of(),
List.of(),
false
);
}
/**
* Builder for fluent construction.
*/
public static Builder builder() {
return new Builder();
}
/** /**
* Generates a region name from the template. * Generates a region name from the template.
* *
@ -74,43 +110,6 @@ public record WorldGuardConfig(
return sb.toString(); return sb.toString();
} }
/**
* Default configuration with auto-region enabled.
* Default flag: block-break=allow (everyone can mine).
* User can add any other flags.
*/
public static WorldGuardConfig defaults() {
return new WorldGuardConfig(
true,
DEFAULT_TEMPLATE,
List.of(),
List.of(),
List.of("block-break=allow"),
true
);
}
/**
* Disabled configuration.
*/
public static WorldGuardConfig disabled() {
return new WorldGuardConfig(
false,
DEFAULT_TEMPLATE,
List.of(),
List.of(),
List.of(),
false
);
}
/**
* Builder for fluent construction.
*/
public static Builder builder() {
return new Builder();
}
public static class Builder { public static class Builder {
private boolean enabled = true; private boolean enabled = true;
private String template = DEFAULT_TEMPLATE; private String template = DEFAULT_TEMPLATE;
@ -153,3 +152,4 @@ public record WorldGuardConfig(
return new WorldGuardConfig(enabled, template, owners, members, flags, protectOnCreate); return new WorldGuardConfig(enabled, template, owners, members, flags, protectOnCreate);
} }
} }
}

View file

@ -7,11 +7,7 @@ import dev.loki.lomines.gui.mine.edit.blocks.BlockMaterialSelectionGui;
import dev.loki.lomines.gui.mine.edit.blocks.BlocksGui; import dev.loki.lomines.gui.mine.edit.blocks.BlocksGui;
import dev.loki.lomines.gui.mine.edit.reset.ResetGui; import dev.loki.lomines.gui.mine.edit.reset.ResetGui;
import dev.loki.lomines.gui.mine.edit.rewards.RewardsGui; import dev.loki.lomines.gui.mine.edit.rewards.RewardsGui;
import dev.loki.lomines.gui.mine.holder.BlockMaterialSelectionGuiHolder; import dev.loki.lomines.gui.mine.holder.*;
import dev.loki.lomines.gui.mine.holder.BlocksGuiHolder;
import dev.loki.lomines.gui.mine.holder.ResetGuiHolder;
import dev.loki.lomines.gui.mine.holder.RewardsGuiHolder;
import dev.loki.lomines.gui.mine.holder.MineEditGuiHolder;
import dev.loki.lomines.gui.mine.main.MineEditGui; import dev.loki.lomines.gui.mine.main.MineEditGui;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;

View file

@ -17,7 +17,8 @@ import java.util.Collection;
*/ */
public final class BlockUpdateUtil { public final class BlockUpdateUtil {
private BlockUpdateUtil() {} private BlockUpdateUtil() {
}
/** /**
* Sends a block update to all players who can see the specified chunk. * Sends a block update to all players who can see the specified chunk.
@ -109,7 +110,7 @@ public final class BlockUpdateUtil {
Chunk chunk = world.getChunkAt(chunkX, chunkZ); Chunk chunk = world.getChunkAt(chunkX, chunkZ);
if (chunk.isLoaded()) { if (chunk.isLoaded()) {
for (Player player : world.getPlayers()) { for (Player player : world.getPlayers()) {
player.sendChunkUpdate(chunk); player.getWorld().refreshChunk(chunk.getX(), chunk.getZ());
} }
} }
} }
@ -135,4 +136,25 @@ public final class BlockUpdateUtil {
double renderDistance = player.getClientViewDistance() * 16; double renderDistance = player.getClientViewDistance() * 16;
return player.getLocation().distanceSquared(loc) <= renderDistance * renderDistance; return player.getLocation().distanceSquared(loc) <= renderDistance * renderDistance;
} }
/**
* Finds a safe teleport location around the target by searching upward.
* A location is safe when feet and head blocks are non-solid.
*/
public static Location findSafeTeleportLocation(Location target, int maxBlocksUp) {
if (target == null || target.getWorld() == null) {
return target;
}
Location base = target.clone();
for (int i = 0; i <= Math.max(0, maxBlocksUp); i++) {
Location candidate = base.clone().add(0, i, 0);
Block feet = candidate.getBlock();
Block head = candidate.clone().add(0, 1, 0).getBlock();
if (!feet.getType().isSolid() && !head.getType().isSolid()) {
return candidate;
}
}
return base;
}
} }

View file

@ -166,8 +166,8 @@ public final class ChunkUtils {
@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 other)) return false; if (!(obj instanceof ChunkCoordinate(int x1, int z1))) return false;
return x == other.x && z == other.z; return x == x1 && z == z1;
} }
} }

View file

@ -10,7 +10,8 @@ import org.bukkit.block.Block;
*/ */
public final class SafeTeleportUtil { public final class SafeTeleportUtil {
private SafeTeleportUtil() {} private SafeTeleportUtil() {
}
/** /**
* Finds a safe teleport location near the given destination. * Finds a safe teleport location near the given destination.

View file

@ -11,7 +11,8 @@ import org.bukkit.entity.Player;
*/ */
final class ParticleUtil { final class ParticleUtil {
private ParticleUtil() {} private ParticleUtil() {
}
static void showCornerParticles(Player player, Location loc, Color color) { static void showCornerParticles(Player player, Location loc, Color color) {
World world = loc.getWorld(); World world = loc.getWorld();

View file

@ -1,6 +1,8 @@
package dev.loki.lomines.wand; package dev.loki.lomines.wand;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.wand.group.GroupWandItem;
import dev.loki.lomines.wand.group.GroupWandSession;
import org.bukkit.Color; import org.bukkit.Color;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -16,13 +18,12 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public final class WandParticleService { public final class WandParticleService {
private final LoMinesPlugin plugin;
private final Map<UUID, BukkitRunnable> activeTasks = new ConcurrentHashMap<>();
private static final Color COLOR_POS1 = Color.LIME; private static final Color COLOR_POS1 = Color.LIME;
private static final Color COLOR_POS2 = Color.AQUA; private static final Color COLOR_POS2 = Color.AQUA;
private static final Color COLOR_EDGE = Color.YELLOW; private static final Color COLOR_EDGE = Color.YELLOW;
private static final Color COLOR_READY = Color.GREEN; private static final Color COLOR_READY = Color.GREEN;
private final LoMinesPlugin plugin;
private final Map<UUID, BukkitRunnable> activeTasks = new ConcurrentHashMap<>();
public WandParticleService(LoMinesPlugin plugin) { public WandParticleService(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;

View file

@ -13,7 +13,7 @@ reset:
percent-enabled: false percent-enabled: false
# Commands to run on reset (%mine%, %player%, %blocks% placeholders) # Commands to run on reset (%mine%, %player%, %blocks% placeholders)
commands: [] commands: [ ]
# - "broadcast Mine %mine% has been reset!" # - "broadcast Mine %mine% has been reset!"
# - "eco give %player% 10" # - "eco give %player% 10"
@ -28,7 +28,7 @@ blocks:
# For MASK mode: which block marks fill positions # For MASK mode: which block marks fill positions
mask: mask:
marker: "pink_concrete" marker: "pink_concrete"
positions: [] positions: [ ]
# UI settings # UI settings
ui: ui:
@ -43,7 +43,7 @@ ui:
timer-format: "mm:ss" timer-format: "mm:ss"
# Rewards for mining specific blocks # Rewards for mining specific blocks
rewards: [] rewards: [ ]
# Example: # Example:
# - chance: 5.0 # - chance: 5.0
# blocks: # blocks:
@ -56,7 +56,7 @@ rewards: []
# - "<gray>Mined from the depths" # - "<gray>Mined from the depths"
# commands: # commands:
# - "eco give %player% 100" # - "eco give %player% 100"
# prevent-drops: true # prevent-drops: true
# Teleport players on reset # Teleport players on reset
teleport: teleport:
@ -84,12 +84,12 @@ worldguard:
region-template: "{mine_name}_{random_4}" region-template: "{mine_name}_{random_4}"
# Region owners (player names or "uuid:<uuid>") # Region owners (player names or "uuid:<uuid>")
owners: [] owners: [ ]
# - "player_name" # - "player_name"
# - "uuid:12345678-1234-1234-1234-123456789012" # - "uuid:12345678-1234-1234-1234-123456789012"
# Region members (player names or "uuid:<uuid>") # Region members (player names or "uuid:<uuid>")
members: [] members: [ ]
# WorldGuard flags to set # WorldGuard flags to set
# Можно добавить любые другие флаги: pvp, tnt, build, и т.д. # Можно добавить любые другие флаги: pvp, tnt, build, и т.д.

View file

@ -70,7 +70,7 @@ reset:
broadcast: "&#00FF00Шахта &#FFFF00{mine} &#00FF00сброшена!" broadcast: "&#00FF00Шахта &#FFFF00{mine} &#00FF00сброшена!"
# Команды при сбросе (опционально) # Команды при сбросе (опционально)
commands: [] commands: [ ]
# - "eco give <player> 100" # - "eco give <player> 100"
# - "broadcast Шахта {mine} сброшена!" # - "broadcast Шахта {mine} сброшена!"
@ -97,7 +97,7 @@ teleport:
# ============================================ # ============================================
# Выдаются с определённым шансом при ломании блоков # Выдаются с определённым шансом при ломании блоков
rewards: [] rewards: [ ]
# Пример награды: # Пример награды:
# - chance: 0.001 # 0.1% шанс # - chance: 0.001 # 0.1% шанс
# prevent-drops: false # Отменить дроп блока? # prevent-drops: false # Отменить дроп блока?
@ -109,7 +109,7 @@ rewards: []
# amount: 1 # amount: 1
# name: "&#00FFFFРедкий алмаз" # name: "&#00FFFFРедкий алмаз"
# commands: # Команды (опционально) # commands: # Команды (опционально)
# - "eco give <player> 1000" # - "eco give <player> 1000"
# ============================================ # ============================================
# ИНТЕРФЕЙС (UI) # ИНТЕРФЕЙС (UI)
@ -144,7 +144,6 @@ ui:
# WORLDGUARD ИНТЕГРАЦИЯ (опционально) # WORLDGUARD ИНТЕГРАЦИЯ (опционально)
# ============================================ # ============================================
# Создаёт регион для защиты шахты # Создаёт регион для защиты шахты
# По умолчанию: только block-break=allow (все могут ломать)
# Можно добавить ЛЮБЫЕ флаги WorldGuard! # Можно добавить ЛЮБЫЕ флаги WorldGuard!
worldguard: worldguard:
enabled: false enabled: false
@ -154,10 +153,10 @@ worldguard:
region-template: "mine_{mine_name}_{random_4}" region-template: "mine_{mine_name}_{random_4}"
# Владельцы региона (uuid:xxx или имя) # Владельцы региона (uuid:xxx или имя)
owners: [] owners: [ ]
# Участники региона # Участники региона
members: [] members: [ ]
# Флаги WorldGuard. По умолчанию: только block-break=allow # Флаги WorldGuard. По умолчанию: только block-break=allow
# МОЖНО ДОБАВИТЬ ЛЮБЫЕ ФЛАГИ: # МОЖНО ДОБАВИТЬ ЛЮБЫЕ ФЛАГИ:

View file

@ -1,8 +1,9 @@
package dev.loki.lomines.command; package dev.loki.lomines.command;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.command.admin.AdminCommands;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.Mines; import dev.loki.lomines.core.mine.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;

View file

@ -1,11 +1,14 @@
package dev.loki.lomines.core; package dev.loki.lomines.core;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.block.BukkitBlockSetter; import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.block.ItemsAdderBlockSetter; import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.block.OraxenBlockSetter; import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.MineConfig; import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.util.location.Cuboid;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -13,7 +16,6 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -33,85 +35,74 @@ class MineTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
MockitoAnnotations.openMocks(this); MockitoAnnotations.openMocks(this);
when(world.getName()).thenReturn("world");
} }
@Test @Test
void testCreateBlockSetter_WithOraxenPrefix_CreatesOraxenBlockSetter() { void testCreateBlockSetter_WithOraxenPrefix_CreatesBukkitBlockSetter() {
// Arrange // Oraxen integration is disabled — constructor throws IllegalArgumentException
Map<String, Double> blocks = new HashMap<>(); Map<BlockKey, Double> blocks = new HashMap<>();
blocks.put("oraxen:custom_ore", 50.0); blocks.put(new BlockKey.Oraxen("custom_ore"), 50.0);
blocks.put("oraxen:rare_ore", 30.0); blocks.put(new BlockKey.Oraxen("rare_ore"), 30.0);
blocks.put(new BlockKey.Oraxen("other_ore"), 20.0);
MineConfig config = createTestConfig(blocks); MineConfig config = createTestConfig(blocks);
// Act assertThrows(IllegalArgumentException.class,
Mine mine = new Mine("test_mine", config, plugin); () -> new Mine("test_mine", config, plugin),
"Expected IllegalArgumentException when Oraxen integration is disabled");
// Assert
assertNotNull(mine.getBlockSetter());
assertInstanceOf(OraxenBlockSetter.class, mine.getBlockSetter(), "Expected OraxenBlockSetter for oraxen: prefix");
} }
@Test @Test
void testCreateBlockSetter_WithItemsAdderPrefix_CreatesItemsAdderBlockSetter() { void testCreateBlockSetter_WithItemsAdderPrefix_CreatesBukkitBlockSetter() {
// Arrange // ItemsAdder integration is disabled — constructor throws IllegalArgumentException
Map<String, Double> blocks = new HashMap<>(); Map<BlockKey, Double> blocks = new HashMap<>();
blocks.put("itemsadder:custom_block", 60.0); blocks.put(new BlockKey.ItemsAdder("custom_block"), 60.0);
blocks.put("itemsadder:special_block", 40.0); blocks.put(new BlockKey.ItemsAdder("special_block"), 40.0);
MineConfig config = createTestConfig(blocks); MineConfig config = createTestConfig(blocks);
// Act assertThrows(IllegalArgumentException.class,
Mine mine = new Mine("test_mine", config, plugin); () -> new Mine("test_mine", config, plugin),
"Expected IllegalArgumentException when ItemsAdder integration is disabled");
// Assert
assertNotNull(mine.getBlockSetter());
assertInstanceOf(ItemsAdderBlockSetter.class, mine.getBlockSetter(), "Expected ItemsAdderBlockSetter for itemsadder: prefix");
} }
@Test @Test
void testCreateBlockSetter_WithoutPrefix_CreatesBukkitBlockSetter() { void testCreateBlockSetter_WithoutPrefix_CreatesBukkitBlockSetter() {
// Arrange Map<BlockKey, Double> blocks = new HashMap<>();
Map<String, Double> blocks = new HashMap<>(); blocks.put(new BlockKey.Vanilla(Material.STONE), 50.0);
blocks.put("STONE", 50.0); blocks.put(new BlockKey.Vanilla(Material.COAL_ORE), 30.0);
blocks.put("COAL_ORE", 30.0); blocks.put(new BlockKey.Vanilla(Material.IRON_ORE), 20.0);
blocks.put("IRON_ORE", 20.0);
MineConfig config = createTestConfig(blocks); MineConfig config = createTestConfig(blocks);
// Act
Mine mine = new Mine("test_mine", config, plugin); Mine mine = new Mine("test_mine", config, plugin);
// Assert
assertNotNull(mine.getBlockSetter()); assertNotNull(mine.getBlockSetter());
assertInstanceOf(BukkitBlockSetter.class, mine.getBlockSetter(), "Expected BukkitBlockSetter for vanilla blocks without prefix"); assertInstanceOf(dev.loki.lomines.block.BukkitBlockSetter.class, mine.getBlockSetter(),
"Expected BukkitBlockSetter for vanilla blocks");
} }
@Test @Test
void testCreateBlockSetter_WithEmptyBlocks_ThrowsException() { void testCreateBlockSetter_WithEmptyBlocks_ThrowsException() {
// Arrange Map<BlockKey, Double> blocks = new HashMap<>();
Map<String, Double> blocks = new HashMap<>();
MineConfig config = createTestConfig(blocks);
// Act & Assert assertThrows(
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class, IllegalArgumentException.class,
() -> new Mine("test_mine", config, plugin) () -> new BlockConfig(blocks, dev.loki.lomines.data.config.block.FillMode.CUBOID, null)
); );
assertEquals("Mine must have at least one block type", exception.getMessage());
} }
private MineConfig createTestConfig(Map<String, Double> blocks) { private MineConfig createTestConfig(Map<BlockKey, Double> blocks) {
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() RegionConfig region = RegionConfig.single(new Cuboid(loc1, loc2));
.selections(List.of(loc1, loc2)) BlockConfig blockConfig = new BlockConfig(blocks, dev.loki.lomines.data.config.block.FillMode.CUBOID, null);
.blocks(blocks)
.resetTicks(6000) return MineConfig.builder("test_mine")
.region(region)
.blocks(blockConfig)
.build(); .build();
} }
} }

View file

@ -1,6 +1,8 @@
package dev.loki.lomines.core; package dev.loki.lomines.core;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.MineTicker;
import dev.loki.lomines.core.mine.Mines;
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;

View file

@ -1,6 +1,8 @@
package dev.loki.lomines.core; package dev.loki.lomines.core;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.Mines;
import dev.lolib.core.LoLogger; import dev.lolib.core.LoLogger;
import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.Scheduler;
@ -9,6 +11,7 @@ import org.bukkit.Material;
import org.bukkit.Server; import org.bukkit.Server;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.junit.jupiter.api.AfterEach;
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,18 +28,36 @@ import static org.mockito.Mockito.*;
/** /**
* Unit tests for Mines registry. * Unit tests for Mines registry.
* Tests loading and creating mines.
*/ */
class MinesTest { class MinesTest {
@TempDir @TempDir
Path tempDir; Path tempDir;
private Mines mines; private Mines mines;
private LoMinesPlugin plugin; private LoMinesPlugin plugin;
private Scheduler scheduler; private Scheduler scheduler;
// Keep Bukkit mock open for the whole test — WorldGuardRegionService calls
// Bukkit.getPluginManager() in the Mines constructor.
private MockedStatic<Bukkit> globalBukkit;
private MockedStatic<Scheduler> globalScheduler;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
globalBukkit = mockStatic(Bukkit.class);
globalScheduler = mockStatic(Scheduler.class);
PluginManager pluginManager = mock(org.bukkit.plugin.PluginManager.class);
Server server = mock(Server.class);
World world = mock(World.class);
BlockData blockData = mock(BlockData.class);
globalBukkit.when(Bukkit::getPluginManager).thenReturn(pluginManager);
globalBukkit.when(Bukkit::getServer).thenReturn(server);
globalBukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
globalBukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData);
when(world.getName()).thenReturn("world");
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);
@ -46,59 +67,37 @@ class MinesTest {
when(plugin.getLogger()).thenReturn(logger); when(plugin.getLogger()).thenReturn(logger);
when(plugin.loLogger()).thenReturn(loLogger); when(plugin.loLogger()).thenReturn(loLogger);
globalScheduler.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
ScheduledTask task = mock(ScheduledTask.class);
when(scheduler.runTimer(any(Runnable.class), anyLong(), anyLong())).thenReturn(task);
mines = new Mines(plugin); mines = new Mines(plugin);
} }
/** @AfterEach
* Helper method to set up Bukkit and Scheduler mocking for tests that create mines. void tearDown() {
*/ if (globalScheduler != null) globalScheduler.close();
private void setupBukkitAndScheduler(MockedStatic<Bukkit> bukkit, MockedStatic<Scheduler> schedulerStatic) { if (globalBukkit != null) globalBukkit.close();
Server server = mock(Server.class);
World world = mock(World.class);
BlockData blockData = mock(BlockData.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
bukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData);
when(world.getName()).thenReturn("world");
schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
// Mock scheduler methods to execute callbacks immediately for testing
ScheduledTask task = mock(ScheduledTask.class);
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
assertDoesNotThrow(() -> mines.loadAll()); assertDoesNotThrow(() -> mines.loadAll());
// 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 Files.createDirectories(tempDir.resolve("mines"));
Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder);
// Load all mines
mines.loadAll(); mines.loadAll();
// 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
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
// 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
@ -118,36 +117,22 @@ class MinesTest {
broadcast-reset: "" broadcast-reset: ""
random-rewards: [] random-rewards: []
"""; """;
Files.writeString(minesFolder.resolve("testmine.yml"), yamlContent);
Path configFile = minesFolder.resolve("testmine.yml");
Files.writeString(configFile, yamlContent);
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines
mines.loadAll(); mines.loadAll();
// 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
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
// 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
@ -167,35 +152,23 @@ class MinesTest {
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
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines
mines.loadAll(); mines.loadAll();
// 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());
assertTrue(mines.find("mine2").isPresent()); assertTrue(mines.find("mine2").isPresent());
assertTrue(mines.find("mine3").isPresent()); assertTrue(mines.find("mine3").isPresent());
} }
}
@Test @Test
void testLoadAllIgnoresNonYamlFiles() throws IOException { void testLoadAllIgnoresNonYamlFiles() throws IOException {
// 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
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
@ -205,196 +178,87 @@ class MinesTest {
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"), "not a mine config");
Files.writeString(minesFolder.resolve("backup.bak"), "Backup file"); Files.writeString(minesFolder.resolve("backup.bak"), "backup file");
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines
mines.loadAll(); mines.loadAll();
// 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
Path minesFolder = tempDir.resolve("mines"); Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder); Files.createDirectories(minesFolder);
Files.writeString(minesFolder.resolve("invalidmine.yml"), "selection.1: world;0;64;0;0.0;0.0\n");
// Create an invalid mine config file (missing required fields)
String invalidYaml = """
selection.1: world;0;64;0;0.0;0.0
""";
Files.writeString(minesFolder.resolve("invalidmine.yml"), invalidYaml);
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines - should not throw, but should log error
assertDoesNotThrow(() -> mines.loadAll()); assertDoesNotThrow(() -> mines.loadAll());
// Invalid mine should not be loaded
assertEquals(0, mines.getAll().size()); assertEquals(0, mines.getAll().size());
// 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
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a new mine
mines.create("newmine"); mines.create("newmine");
// 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());
assertTrue(Files.exists(tempDir.resolve("mines/newmine.yml")));
// Verify config file was created
Path configFile = tempDir.resolve("mines/newmine.yml");
assertTrue(Files.exists(configFile));
// 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 assertFalse(Files.exists(tempDir.resolve("mines")));
Path minesFolder = tempDir.resolve("mines");
assertFalse(Files.exists(minesFolder));
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a new mine
mines.create("testmine"); mines.create("testmine");
assertTrue(Files.isDirectory(tempDir.resolve("mines")));
// Verify mines folder was created
assertTrue(Files.exists(minesFolder));
assertTrue(Files.isDirectory(minesFolder));
}
} }
@Test @Test
void testCreateDuplicateMineThrowsException() throws IOException { void testCreateDuplicateMineThrowsException() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine
mines.create("duplicate"); mines.create("duplicate");
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
// Try to create the same mine again () -> mines.create("duplicate"));
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { assertTrue(ex.getMessage().contains("Mine already exists"));
mines.create("duplicate");
});
assertTrue(exception.getMessage().contains("Mine already exists"));
}
} }
@Test @Test
void testCreateMineIsCaseInsensitive() throws IOException { void testCreateMineIsCaseInsensitive() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine with mixed case
mines.create("TestMine"); mines.create("TestMine");
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
// Try to create the same mine with different case () -> mines.create("testmine"));
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { assertTrue(ex.getMessage().contains("Mine already exists"));
mines.create("testmine");
});
assertTrue(exception.getMessage().contains("Mine already exists"));
}
} }
@Test @Test
void testGetMineByName() throws IOException { void testGetMineByName() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine
mines.create("testmine"); mines.create("testmine");
// 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
Mine mine2 = mines.get("TestMine"); Mine mine2 = mines.get("TestMine");
assertNotNull(mine2); assertNotNull(mine2);
assertEquals("testmine", mine2.getName());
// Same mine instance
assertSame(mine, mine2); assertSame(mine, mine2);
} }
}
@Test @Test
void testGetNonExistentMineThrowsException() { void testGetNonExistentMineThrowsException() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
mines.get("nonexistent"); () -> mines.get("nonexistent"));
}); assertTrue(ex.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
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine
mines.create("testmine"); mines.create("testmine");
// Find existing mine
assertTrue(mines.find("testmine").isPresent()); assertTrue(mines.find("testmine").isPresent());
// Find non-existent mine
assertFalse(mines.find("nonexistent").isPresent()); assertFalse(mines.find("nonexistent").isPresent());
} }
}
} }

View file

@ -1,6 +1,9 @@
package dev.loki.lomines.data; package dev.loki.lomines.data;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.stats.Leaderboard;
import dev.loki.lomines.data.stats.LeaderboardEntry;
import dev.loki.lomines.data.stats.StatsManager;
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;
@ -48,7 +51,7 @@ class LeaderboardCacheIntegrationTest {
// 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).playerId());
// Increment blocks for player2 - this should invalidate cache // Increment blocks for player2 - this should invalidate cache
statsManager.incrementBlocks(player2, "mine1"); statsManager.incrementBlocks(player2, "mine1");
@ -58,8 +61,8 @@ class LeaderboardCacheIntegrationTest {
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).playerId(), "Player1 should still be first");
assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second"); assertEquals(player2, top2.get(1).playerId(), "Player2 should be second");
} }
@Test @Test
@ -73,7 +76,7 @@ class LeaderboardCacheIntegrationTest {
// 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).playerId(), "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++) {
@ -82,10 +85,10 @@ class LeaderboardCacheIntegrationTest {
// 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).playerId(), "Player1 should be first after increments");
assertEquals(110, top2.get(0).getCount(), "Player1 should have 110 blocks"); assertEquals(110, top2.get(0).count(), "Player1 should have 110 blocks");
assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second"); assertEquals(player2, top2.get(1).playerId(), "Player2 should be second");
assertEquals(100, top2.get(1).getCount(), "Player2 should still have 100 blocks"); assertEquals(100, top2.get(1).count(), "Player2 should still have 100 blocks");
} }
@Test @Test
@ -104,7 +107,7 @@ class LeaderboardCacheIntegrationTest {
// 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).count(), "Should have 5 blocks after 5 increments");
} }
@Test @Test
@ -150,7 +153,7 @@ class LeaderboardCacheIntegrationTest {
// 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());
assertEquals(player3, top2.get(0).getPlayerId(), "Should have highest player"); assertEquals(player3, top2.get(0).playerId(), "Should have highest player");
assertEquals(player2, top2.get(1).getPlayerId(), "Should have second highest player"); assertEquals(player2, top2.get(1).playerId(), "Should have second highest player");
} }
} }

View file

@ -1,6 +1,10 @@
package dev.loki.lomines.data; package dev.loki.lomines.data;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.stats.Leaderboard;
import dev.loki.lomines.data.stats.LeaderboardEntry;
import dev.loki.lomines.data.stats.PlayerStats;
import dev.loki.lomines.data.stats.StatsManager;
import dev.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;
@ -51,8 +55,8 @@ class LeaderboardTest {
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).playerId());
assertEquals(100, top.get(0).getCount()); assertEquals(100, top.get(0).count());
} }
@Test @Test
@ -67,9 +71,9 @@ class LeaderboardTest {
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).playerId()); // 200 blocks
assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks assertEquals(player3, top.get(1).playerId()); // 100 blocks
assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks assertEquals(player1, top.get(2).playerId()); // 50 blocks
} }
@Test @Test
@ -93,7 +97,7 @@ class LeaderboardTest {
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).playerId());
} }
@Test @Test
@ -108,7 +112,7 @@ class LeaderboardTest {
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).playerId(), top2.get(0).playerId());
} }
@Test @Test
@ -129,7 +133,7 @@ class LeaderboardTest {
// 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).playerId(), "New player should be first");
} }
@Test @Test
@ -144,9 +148,9 @@ class LeaderboardTest {
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).playerId()); // 200 blocks
assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks assertEquals(player3, top.get(1).playerId()); // 100 blocks
assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks assertEquals(player1, top.get(2).playerId()); // 50 blocks
} }
@Test @Test
@ -159,7 +163,7 @@ class LeaderboardTest {
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).playerId());
} }
@Test @Test
@ -172,7 +176,7 @@ class LeaderboardTest {
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).playerId());
} }
@Test @Test

View file

@ -1,6 +1,8 @@
package dev.loki.lomines.data; package dev.loki.lomines.data;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.stats.PlayerStats;
import dev.loki.lomines.data.stats.StatsManager;
import dev.lolib.core.LoLogger; import dev.lolib.core.LoLogger;
import dev.lolib.scheduler.ScheduledTask; import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.Scheduler;

View file

@ -33,7 +33,8 @@ class UIConfigTest {
true, true,
"<red>{mine}</red>", "<red>{mine}</red>",
100.0, 100.0,
"HH:mm:ss" "HH:mm:ss",
null
); );
assertTrue(config.actionBarEnabled()); assertTrue(config.actionBarEnabled());
@ -44,7 +45,7 @@ class UIConfigTest {
@Test @Test
void testNullFormatDefaults() { void testNullFormatDefaults() {
UIConfig config = new UIConfig(true, null, 50.0, null); UIConfig config = new UIConfig(true, null, 50.0, null, null);
assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat()); assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat());
assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat()); assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat());
@ -52,7 +53,7 @@ class UIConfigTest {
@Test @Test
void testBlankFormatDefaults() { void testBlankFormatDefaults() {
UIConfig config = new UIConfig(true, " ", 50.0, " "); UIConfig config = new UIConfig(true, " ", 50.0, " ", null);
assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat()); assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat());
assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat()); assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat());
@ -60,14 +61,14 @@ class UIConfigTest {
@Test @Test
void testNegativeRangeClamped() { void testNegativeRangeClamped() {
UIConfig config = new UIConfig(true, "test", -10.0, "mm:ss"); UIConfig config = new UIConfig(true, "test", -10.0, "mm:ss", null);
assertEquals(1.0, config.actionBarRange()); assertEquals(1.0, config.actionBarRange());
} }
@Test @Test
void testRangeSquared() { void testRangeSquared() {
UIConfig config = new UIConfig(true, "test", 50.0, "mm:ss"); UIConfig config = new UIConfig(true, "test", 50.0, "mm:ss", null);
assertEquals(2500.0, config.actionBarRangeSquared(), 0.001); assertEquals(2500.0, config.actionBarRangeSquared(), 0.001);
} }
@ -85,7 +86,7 @@ class UIConfigTest {
@Test @Test
void testFormatTimerMmSs() { void testFormatTimerMmSs() {
UIConfig config = new UIConfig(true, "", 0, "mm:ss"); UIConfig config = new UIConfig(true, "", 0, "mm:ss", null);
assertEquals("00:30", config.formatTimer(30)); assertEquals("00:30", config.formatTimer(30));
assertEquals("05:00", config.formatTimer(300)); assertEquals("05:00", config.formatTimer(300));
@ -94,7 +95,7 @@ class UIConfigTest {
@Test @Test
void testFormatTimerHhMmSs() { void testFormatTimerHhMmSs() {
UIConfig config = new UIConfig(true, "", 0, "HH:mm:ss"); UIConfig config = new UIConfig(true, "", 0, "HH:mm:ss", null);
assertEquals("0:00:30", config.formatTimer(30)); assertEquals("0:00:30", config.formatTimer(30));
assertEquals("0:05:00", config.formatTimer(300)); assertEquals("0:05:00", config.formatTimer(300));

View file

@ -1,5 +1,7 @@
package dev.loki.lomines.util; package dev.loki.lomines.util;
import dev.loki.lomines.util.format.ChunkUtils;
import dev.loki.lomines.util.location.Cuboid;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;

View file

@ -1,6 +1,7 @@
package dev.loki.lomines.util; package dev.loki.lomines.util;
import dev.loki.lomines.data.config.parser.ConfigParseException; import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;

View file

@ -1,7 +1,9 @@
package dev.loki.lomines.util; package dev.loki.lomines.util;
import dev.loki.lomines.util.format.TimeFormatter;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;

View file

@ -1,5 +1,6 @@
package dev.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.World; import org.bukkit.World;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;