Compare commits

..

10 commits

181 changed files with 7830 additions and 5873 deletions

25
.classpath Normal file
View file

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

34
.project Normal file
View file

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

View file

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

File diff suppressed because it is too large Load diff

292
README.md
View file

@ -1,219 +1,141 @@
<div align="center">
# LoMines # LoMines
Advanced mine management plugin for Paper 1.21.4+ Multi-region mine management plugin with WorldGuard integration and player statistics for Paper servers.
## Features ![Java](https://img.shields.io/badge/Java-21+-orange?style=flat-square&logo=openjdk&logoColor=white)
![Paper](https://img.shields.io/badge/Paper-1.19.2+-blue?style=flat-square)
![Folia](https://img.shields.io/badge/Folia-supported-purple?style=flat-square)
![License](https://img.shields.io/badge/license-GPLv3-blue?style=flat-square&logo=gnu&logoColor=white)
![version](https://img.shields.io/badge/version-3.0.0-green?style=flat-square)
- **Multiple fill modes**: Cuboid (fill entire region) or Mask (fill marked positions only) [English](#english) | [Русский](#russian)
- **Weighted block system**: Configure different blocks with spawn chances
- **Automatic resets**: Timer-based or percentage-based triggers
- **WorldGuard integration**: Auto-create regions for new mines
- **PlaceholderAPI support**: Display mine info in other plugins
- **GUI editor**: In-game mine configuration editor
- **Player statistics**: Track blocks mined, mines reset, playtime
## Commands </div>
### Admin Commands ---
<a name="english"></a>
## English
### Overview
LoMines is an advanced mine management plugin for Paper 1.19.2+ servers. It supports multiple fill modes, weighted block systems, automatic resets, WorldGuard integration, in-game GUI editor, and player statistics.
### Features
| Feature | Description |
|---------|-------------|
| Multiple fill modes | Cuboid (fill entire region) or Mask (fill marked positions) |
| Weighted blocks | Configure spawn chances per block type |
| Automatic resets | Timer-based or percentage-based triggers |
| WorldGuard integration | Auto-create regions for new mines |
| GUI editor | In-game mine configuration |
| Player statistics | Track blocks mined, resets, playtime |
| Leaderboards | Top players per mine or global |
| Group wand | Create multiple mines from batch selections |
| PlaceholderAPI | Display mine info in other plugins |
| Holograms | DecentHolograms and HolographicDisplays support |
### Commands
| Command | Permission | Description | | Command | Permission | Description |
|---------|------------|-------------| |---------|------------|-------------|
| `/lm create <name>` | `lomines.admin.create` | Create a new mine | | `/lm create <name>` | `lomines.admin` | Create a new mine |
| `/lm delete <name>` | `lomines.admin.delete` | Delete a mine | | `/lm delete <name>` | `lomines.admin` | Delete a mine |
| `/lm reset <name> [silent]` | `lomines.admin.reset` | Reset a mine manually | | `/lm reset <name> [silent]` | `lomines.admin` | Reset a mine |
| `/lm reload` | `lomines.admin.reload` | Reload all mine configs | | `/lm reload` | `lomines.admin` | Reload configs |
| `/lm list` | `lomines.admin.list` | List all mines | | `/lm list` | `lomines.admin` | List all mines |
| `/lm edit <mine>` | `lomines.admin.edit` | Open GUI editor for a mine | | `/lm edit <mine>` | `lomines.admin.edit` | Open GUI editor |
| `/lm setteleport <mine>` | `lomines.admin.setteleport` | Set teleport location (your current position) | | `/lm wand` | `lomines.admin.wand` | Get selection wand |
| `/lm setspawn <mine>` | `lomines.admin.setspawn` | Set safe spawn location for stuck players | | `/lm group <prefix>` | `lomines.admin.wand` | Batch mine creation |
| `/lm clearspawn <mine>` | `lomines.admin.setspawn` | Clear spawn location | | `/lm setteleport <mine>` | `lomines.admin.setteleport` | Set teleport point |
| `/lm maskscan <mine>` | `lomines.admin.maskscan` | Scan mask marker blocks | | `/lm setspawn <mine>` | `lomines.admin.setspawn` | Set safe spawn |
| `/lm maskscan <mine>` | `lomines.admin.maskscan` | Scan mask markers |
### Player Commands | `/lm stats [player]` | `lomines.stats` | View statistics |
| Command | Permission | Description |
|---------|------------|-------------|
| `/lm stats [player]` | `lomines.stats` | View mining statistics |
| `/lm top [mine] [limit]` | `lomines.stats` | View leaderboard | | `/lm top [mine] [limit]` | `lomines.stats` | View leaderboard |
### Wand Commands ### Placeholders
| Command | Permission | Description | | Placeholder | Description |
|---------|------------|-------------| |-------------|-------------|
| `/lm wand` | `lomines.admin.wand` | Get the region selection wand | | `%lomines_mine_<name>_blocks%` | Current blocks in mine |
| `/lm group <prefix>` | `lomines.admin.wand` | Create mines from group wand selections | | `%lomines_mine_<name>_percent%` | Fill percentage |
| `%lomines_mine_<name>_remaining%` | Remaining blocks |
| `%lomines_mine_<name>_resettime%` | Time since reset |
| `%lomines_player_blocksmined%` | Total blocks mined |
| `%lomines_player_rank%` | Leaderboard rank |
| `%lomines_count%` | Total mines count |
## Permissions ### Dependencies
```yaml - Required: Paper 1.19.2+, Java 21+
lomines.admin: - Optional: PlaceholderAPI, WorldGuard, DecentHolograms, HolographicDisplays, Oraxen, ItemsAdder
description: All admin commands
default: op
children:
lomines.use: true
lomines.admin.create: true
lomines.admin.delete: true
lomines.admin.reset: true
lomines.admin.reload: true
lomines.admin.list: true
lomines.admin.edit: true
lomines.admin.setteleport: true
lomines.admin.setspawn: true
lomines.admin.maskscan: true
lomines.admin.wand: true
lomines.stats: true
lomines.stats.others: true
lomines.use: ### Installation
description: Basic usage
default: true
lomines.stats: 1. Drop the jar into `plugins/`
description: View own stats and leaderboards 2. Restart the server
default: true 3. Configure `plugins/LoMines/mines/` and `plugins/LoMines/defaults.yml`
lomines.stats.others: ---
description: View other players' stats
default: op
```
## Placeholders (PlaceholderAPI) <a name="russian"></a>
### Mine Information ## Русский
| Placeholder | Description | Example | ### Обзор
|-------------|-------------|---------|
| `%lomines_mine_<name>_name%` | Mine name | `mymine` |
| `%lomines_mine_<name>_blocks%` | Current blocks | `8542` |
| `%lomines_mine_<name>_total%` | Total volume | `10000` |
| `%lomines_mine_<name>_percent%` | Fill percentage (1 decimal) | `85.4` |
| `%lomines_mine_<name>_percentint%` | Fill percentage (integer) | `85` |
| `%lomines_mine_<name>_world%` | World name | `world` |
| `%lomines_mine_<name>_remaining%` | Remaining blocks | `1458` |
| `%lomines_mine_<name>_resettime%` | Time since reset (mm:ss) | `03:45` |
| `%lomines_mine_<name>_resetseconds%` | Seconds since reset | `225` |
### Player Statistics LoMines - продвинутый плагин управления шахтами для Paper 1.19.2+ серверов. Поддерживает несколько режимов заполнения, взвешенные блоки, автоматические сбросы, интеграцию с WorldGuard, GUI-редактор и статистику игроков.
| Placeholder | Description | Example | ### Возможности
|-------------|-------------|---------|
| `%lomines_player_blocksmined%` | Total blocks mined | `15234` |
| `%lomines_player_minesreset%` | Mines reset count | `45` |
| `%lomines_player_playtime%` | Formatted play time | `2h 15m` |
| `%lomines_player_rank%` | Leaderboard rank | `3` |
### Global | Возможность | Описание |
|-------------|----------|
| Режимы заполнения | Cuboid (весь регион) или Mask (по меткам) |
| Взвешенные блоки | Настройка шансов появления для каждого блока |
| Авто-сбросы | По таймеру или по проценту добычи |
| WorldGuard | Авто-создание регионов для новых шахт |
| GUI-редактор | Настройка шахт прямо в игре |
| Статистика | Добыто блоков, сбросы, время игры |
| Лидерборды | Топ игроков по шахте или глобально |
| Групповая палка | Создание нескольких шахт за раз |
| PlaceholderAPI | Информация о шахтах в других плагинах |
| Голограммы | Поддержка DecentHolograms и HolographicDisplays |
| Placeholder | Description | Example | ### Команды
|-------------|-------------|---------|
| `%lomines_count%` | Total number of mines | `12` |
## Configuration | Команда | Право | Описание |
|---------|-------|----------|
| `/lm create <имя>` | `lomines.admin` | Создать шахту |
| `/lm delete <имя>` | `lomines.admin` | Удалить шахту |
| `/lm reset <имя>` | `lomines.admin` | Сбросить шахту |
| `/lm edit <имя>` | `lomines.admin.edit` | Открыть GUI-редактор |
| `/lm wand` | `lomines.admin.wand` | Получить палку выделения |
| `/lm stats [игрок]` | `lomines.stats` | Статистика |
| `/lm top [шахта]` | `lomines.stats` | Лидерборд |
### defaults.yml ### Зависимости
Default configuration applied to all mines: - Обязательные: Paper 1.19.2+, Java 21+
- Опциональные: PlaceholderAPI, WorldGuard, DecentHolograms, Oraxen, ItemsAdder
```yaml ### Установка
# Reset settings
reset:
interval: "5m" # Reset interval (30s, 5m, 2h, 1d)
percent-trigger: 10.0 # Trigger reset when X% of blocks mined
percent-enabled: false # Enable percentage trigger
commands: [] # Commands to run on reset
broadcast: "" # Broadcast message on reset
# Block settings 1. Положите jar в папку `plugins/`
blocks: 2. Перезапустите сервер
fill-mode: CUBOID # CUBOID or MASK 3. Настройте `plugins/LoMines/mines/` и `plugins/LoMines/defaults.yml`
mask:
marker: "pink_concrete" # Marker block for mask mode
# Teleport settings ---
teleport:
enabled: false
location: ""
# Safe spawn for stuck players ### Links
player-spawn:
enabled: false
location: ""
# UI settings - [Releases](../../releases)
ui: - [Issues](../../issues)
actionbar: - [License](LICENSE)
enabled: true
format: "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})"
range: 50.0
# WorldGuard integration ### License
worldguard:
enabled: false
region-template: "{mine_name}_{random_4}"
owners: []
members: []
flags:
- "passthrough=deny"
- "build=allow"
```
## Usage Examples GNU General Public License v3.0
### Creating a Simple Mine
```bash
# 1. Get the wand
/lm wand
# 2. Select two corners (left and right click)
# 3. Create the mine
/lm create mymine
# 4. Edit the config file or use GUI
/lm edit mymine
```
### Setting Up Teleport
```bash
# Stand where players should be teleported
/lm setteleport mymine
# Stand where STUCK players should spawn (optional, falls back to teleport location)
/lm setspawn mymine
```
### Using Mask Fill Mode
```bash
# 1. Paint positions with pink_concrete (or configured marker)
# 2. Scan the markers
/lm maskscan mymine
# Mine will now only fill at marked positions
```
### WorldGuard Auto-Regions
Enable in `plugins/LoMines/mines/_defaults.yml`:
```yaml
worldguard:
enabled: true
region-template: "mine_{mine_name}_{random_4}"
flags:
- "passthrough=deny"
- "build=allow"
```
When you create a mine, a WorldGuard region `mine_mymine_7392` is automatically created.
## Support
- Paper 1.21.4+
- Java 21+
- Optional: PlaceholderAPI, WorldGuard
## License
MIT License

View file

@ -1,6 +1,7 @@
plugins { plugins {
java java
id("com.github.johnrengelman.shadow") version "8.1.1" id("com.github.johnrengelman.shadow") version "8.1.1"
checkstyle
} }
group = "com.loki" group = "com.loki"
@ -13,79 +14,88 @@ 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/")
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
maven("https://maven.enginehub.org/repo/")
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") { include("lolib*.jar") }
if (lolibTree.files.isEmpty()) {
throw GradleException(
"Missing libs/lolib*.jar — add LoAPI fat jar to ${rootProject.file("libs").absolutePath}"
)
}
dependencies { dependencies {
// Paper API 1.21.4 (includes Adventure API)
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
implementation("org.apache.commons:commons-math3:3.6.1")
implementation("commons-io:commons-io:2.18.0")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.13")
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10")
// LoAPI (local)
implementation(lolibTree)
// Runtime dependencies (loaded via DependencyManager from LoAPI)
// These are marked as compileOnly because they will be downloaded at runtime
compileOnly("org.apache.commons:commons-math3:3.6.1")
compileOnly("commons-io:commons-io:2.18.0")
// Adventure API (included in Paper, but explicit for clarity)
compileOnly("net.kyori:adventure-api:4.17.0")
compileOnly("net.kyori:adventure-text-minimessage:4.17.0")
// Optional plugin integrations (soft dependencies)
// compileOnly("com.github.oraxen:oraxen:1.180.0")
// compileOnly("com.github.LoneDev6:api-itemsadder:3.6.4")
// Test dependencies
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("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") testImplementation("org.mockito:mockito-inline:5.2.0")
testImplementation("org.apache.commons:commons-math3:3.6.1") testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.110.0") {
testImplementation("commons-io:commons-io:2.18.0") exclude(group = "org.junit.jupiter", module = "junit-jupiter-api")
}
testImplementation("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
testImplementation("net.kyori:adventure-api:4.17.0")
testImplementation("net.kyori:adventure-text-minimessage:4.17.0")
testImplementation("com.sk89q.worldguard:worldguard-bukkit:7.0.13")
}
checkstyle {
toolVersion = "10.21.4"
configFile = file("config/checkstyle/checkstyle.xml")
isIgnoreFailures = false
maxErrors = 0
maxWarnings = 0
}
tasks.withType<Checkstyle>().configureEach {
reports {
xml.required.set(true)
html.required.set(true)
}
} }
tasks { 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 {
dependsOn(shadowJar) dependsOn(shadowJar)
} }
test { test {
useJUnitPlatform() useJUnitPlatform()
} }
processResources { processResources {
val props = mapOf( val props = mapOf(
"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)
} }
} }
} }

View file

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

Binary file not shown.

251
gradlew vendored Executable 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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -0,0 +1,96 @@
package dev.loki.lomines.command.admin.info;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Map;
public class InfoCommand {
private final LoMinesPlugin plugin;
public InfoCommand(LoMinesPlugin plugin) {
this.plugin = plugin;
}
public void handle(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm info <mine>", NamedTextColor.RED));
return;
}
String mineName = args[0];
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
var config = mine.getConfig();
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
sender.sendMessage(Component.text("§6§lИнформация о шахте: §f" + mine.getName()));
sender.sendMessage(Component.text(""));
sender.sendMessage(Component.text("§7Мир: §f" + config.worldName()));
sender.sendMessage(Component.text("§7Блоков: §f" + mine.getBlocks() + "§7/§f" + mine.getTotalVolume() +
" §7(§f" + String.format("%.1f", mine.getPercentFilled()) + "%§7)"));
printRegions(sender, mine.getRegions());
printBlocks(sender, config.blocks().weights());
printResetAndTeleport(sender, config);
sender.sendMessage(Component.text("§6§lНаграды: §f" + config.rewards().entries().size() + " записей"));
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}
private void printRegions(CommandSender sender, List<Cuboid> regions) {
sender.sendMessage(Component.text("§6§lРегионы:"));
for (int i = 0; i < regions.size(); i++) {
Cuboid r = regions.get(i);
sender.sendMessage(Component.text("§7 #" + (i + 1) + ": §f" +
r.getMinX() + "," + r.getMinY() + "," + r.getMinZ() + " §7-> §f" +
r.getMaxX() + "," + r.getMaxY() + "," + r.getMaxZ() +
" §7(§f" + r.getVolume() + " §7блоков)"));
}
sender.sendMessage(Component.text(""));
}
private void printBlocks(CommandSender sender, Map<BlockKey, Double> weights) {
sender.sendMessage(Component.text("§6§lБлоки:"));
weights.entrySet().stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.limit(10)
.forEach(e -> sender.sendMessage(Component.text(
"§7 " + formatBlockKey(e.getKey()) + ": §f" + String.format("%.1f", e.getValue() * 100) + "%")));
if (weights.size() > 10) {
sender.sendMessage(Component.text("§7 ... и ещё " + (weights.size() - 10) + " блоков"));
}
}
private void printResetAndTeleport(CommandSender sender, MineConfig config) {
sender.sendMessage(Component.text("§6§lНастройки сброса:"));
sender.sendMessage(Component.text("§7 Интервал: §f" + config.reset().intervalDisplay()));
sender.sendMessage(Component.text("§7 Триггер по %: §f" +
(config.reset().isPercentTriggerEnabled() ? config.reset().percentTrigger() + "%" : "выкл")));
sender.sendMessage(Component.text("§6§lТелепортация:"));
sender.sendMessage(Component.text("§7 Точка телепорта: §f" +
(config.teleport().enabled() ? "§aустановлена" : "§7не задана")));
sender.sendMessage(Component.text("§7 Точка спавна: §f" +
(config.playerSpawn().enabled() ? "§aустановлена" : "§7используется точка телепорта")));
}
private String formatBlockKey(BlockKey key) {
return switch (key) {
case BlockKey.Vanilla v -> v.material().name().toLowerCase();
case BlockKey.Oraxen o -> "oraxen:" + o.id();
case BlockKey.ItemsAdder i -> "ia:" + i.id();
};
}
}

View file

@ -0,0 +1,127 @@
package dev.loki.lomines.command.admin.info;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
public final class RegionActionHandler {
private final LoMinesPlugin plugin;
public RegionActionHandler(LoMinesPlugin plugin) {
this.plugin = plugin;
}
public void addRegion(CommandSender sender, String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED));
return;
}
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
Cuboid newRegion = resolveNewRegion(player, mine);
if (newRegion == null) {
return;
}
List<Cuboid> currentRegions = new ArrayList<>(mine.getRegions());
currentRegions.add(newRegion);
updateMineRegions(mine, new RegionConfig(currentRegions));
sendRegionAdded(player, newRegion, currentRegions.size());
}
private Cuboid resolveNewRegion(Player player, Mine mine) {
var wandManager = plugin.getGroupWandManager();
var session = wandManager.getSession(player.getUniqueId());
int slot = session.getActiveSlot();
var pos1 = session.getPos1(slot);
var pos2 = session.getPos2(slot);
if (pos1 == null || pos2 == null) {
player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка"));
return null;
}
Cuboid newRegion = new Cuboid(pos1, pos2);
String mineWorld = mine.getConfig().region().worldName();
String regionWorld = newRegion.getWorld().getName();
if (!mineWorld.equals(regionWorld)) {
player.sendMessage(Component.text("§cРегион должен быть в мире §f" + mineWorld + "§c, а не §f" + regionWorld));
return null;
}
session.clearCorners(slot);
return newRegion;
}
private void sendRegionAdded(Player player, Cuboid newRegion, int count) {
player.sendMessage(Component.text("§aДобавлен регион #" + count));
player.sendMessage(Component.text("§7От: §f" + newRegion.getMinX() + ", " + newRegion.getMinY() + ", " + newRegion.getMinZ()));
player.sendMessage(Component.text("§7До: §f" + newRegion.getMaxX() + ", " + newRegion.getMaxY() + ", " + newRegion.getMaxZ()));
player.sendMessage(Component.text("§7Объём: §f" + newRegion.getVolume() + " §7блоков"));
}
public void removeRegion(CommandSender sender, String mineName, int index) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
List<Cuboid> currentRegions = new ArrayList<>(mine.getRegions());
if (currentRegions.size() <= 1) {
sender.sendMessage(Component.text("§cНельзя удалить последний регион! Удалите всю шахту."));
return;
}
if (index < 1 || index > currentRegions.size()) {
sender.sendMessage(Component.text("§cНеверный номер региона. Доступны: 1-" + currentRegions.size()));
return;
}
Cuboid removed = currentRegions.remove(index - 1);
var newRegionConfig = new RegionConfig(currentRegions);
updateMineRegions(mine, newRegionConfig);
sender.sendMessage(Component.text("§aУдалён регион #" + index));
sender.sendMessage(Component.text("§7Был: §f" + removed.getMinX() + ", " + removed.getMinY() + ", " + removed.getMinZ() +
" §7-> §f" + removed.getMaxX() + ", " + removed.getMaxY() + ", " + removed.getMaxZ()));
sender.sendMessage(Component.text("§7Осталось регионов: §f" + currentRegions.size()));
}
private void updateMineRegions(Mine mine, RegionConfig newConfig) {
var oldConfig = mine.getConfig();
var newMineConfig = new MineConfig(
oldConfig.name(),
newConfig,
oldConfig.blocks(),
oldConfig.reset(),
oldConfig.rewards(),
oldConfig.teleport(),
oldConfig.ui(),
oldConfig.worldGuard(),
oldConfig.playerSpawn()
);
try {
plugin.getMines().updateMineConfig(mine.getName(), newMineConfig);
} catch (Exception e) {
throw new IllegalStateException("Failed to update mine regions", e);
}
}
}

View file

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

View file

@ -0,0 +1,90 @@
package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.util.ErrorHandler;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;
import java.util.Collection;
public class AdminCommands {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
private final MineActionHandler actionHandler;
public AdminCommands(LoMinesPlugin plugin) {
this.plugin = plugin;
this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
this.actionHandler = new MineActionHandler(plugin, mines, errorHandler);
}
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "create" -> handleCreate(sender, args);
case "delete" -> handleDelete(sender, args);
case "edit" -> handleEdit(sender, args);
case "reset" -> handleReset(sender, args);
case "reload" -> actionHandler.reload(sender);
case "list" -> handleList(sender);
default -> {}
}
}
private void handleCreate(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm create <name>"));
return;
}
actionHandler.create(sender, args[0]);
}
private void handleDelete(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm delete <name>"));
return;
}
actionHandler.delete(sender, args[0]);
}
private void handleEdit(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm edit <mine>"));
return;
}
actionHandler.edit(sender, args[0]);
}
private void handleReset(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm reset <mine> [silent]"));
return;
}
boolean silent = args.length > 1 && (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("silent"));
actionHandler.reset(sender, args[0], silent);
}
private void handleList(CommandSender sender) {
Collection<Mine> allMines = mines.getAll();
if (allMines.isEmpty()) {
sender.sendMessage(Component.text("No mines found"));
return;
}
sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ==="));
for (Mine mine : allMines) {
String status = formatMineStatus(mine);
sender.sendMessage(Component.text("- " + mine.getName() + " " + status));
}
}
private String formatMineStatus(Mine mine) {
int blocks = mine.getBlocks();
int total = mine.getTotalVolume();
double percent = mine.getPercentFilled();
return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent);
}
}

View file

@ -0,0 +1,92 @@
package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.model.MineConfig;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public class CopyCommand {
private final LoMinesPlugin plugin;
public CopyCommand(LoMinesPlugin plugin) {
this.plugin = plugin;
}
public void handle(CommandSender sender, String[] args) {
if (args.length < 2) {
sender.sendMessage(Component.text("Usage: /lm copy <from> <to>", NamedTextColor.RED));
return;
}
String fromMineName = args[0];
String toMineName = args[1];
MinePair pair = resolveMines(sender, fromMineName, toMineName);
if (pair == null) {
return;
}
var sourceConfig = pair.fromMine().getConfig();
MineConfig newTargetConfig = buildCopiedConfig(pair.toMine(), sourceConfig);
try {
plugin.getMines().updateMineConfig(pair.toMine().getName(), newTargetConfig);
} catch (Exception e) {
sender.sendMessage(Component.text("Ошибка сохранения: " + e.getMessage(), NamedTextColor.RED));
return;
}
sendCopySummary(sender, pair.fromMine(), pair.toMine(), sourceConfig);
}
private record MinePair(Mine fromMine, Mine toMine) {
}
private MinePair resolveMines(CommandSender sender, String fromMineName, String toMineName) {
Mine fromMine = plugin.getMines().find(fromMineName).orElse(null);
if (fromMine == null) {
sender.sendMessage(Component.text("Исходная шахта не найдена: " + fromMineName, NamedTextColor.RED));
return null;
}
Mine toMine = plugin.getMines().find(toMineName).orElse(null);
if (toMine == null) {
sender.sendMessage(Component.text("Целевая шахта не найдена: " + toMineName, NamedTextColor.RED));
return null;
}
if (fromMine.getName().equalsIgnoreCase(toMine.getName())) {
sender.sendMessage(Component.text("Нельзя копировать шахту саму в себя!", NamedTextColor.RED));
return null;
}
return new MinePair(fromMine, toMine);
}
private MineConfig buildCopiedConfig(Mine toMine, MineConfig sourceConfig) {
var targetConfig = toMine.getConfig();
return MineConfig.builder(toMine.getName())
.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();
}
private void sendCopySummary(CommandSender sender, Mine fromMine, Mine toMine, MineConfig sourceConfig) {
sender.sendMessage(Component.text("§aКонфигурация скопирована из §f" + fromMine.getName() +
" §aв §f" + toMine.getName()));
sender.sendMessage(Component.text("§7Скопировано:"));
sender.sendMessage(Component.text(" §7• Блоки: §f" + sourceConfig.blocks().weights().size() + " типов"));
sender.sendMessage(Component.text(" §7• Настройки сброса"));
sender.sendMessage(Component.text(" §7• Точки телепорта/спавна"));
sender.sendMessage(Component.text(" §7• Награды: §f" + sourceConfig.rewards().entries().size() + " записей"));
sender.sendMessage(Component.text("§7§oПримечание: регионы и позиции не скопированы"));
}
}

View file

@ -1,45 +1,31 @@
package dev.loki.lomines.command.admin; package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.gui.mine.main.MineEditGui; import dev.loki.lomines.gui.mine.main.MineEditGui;
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.Command;
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 org.bukkit.entity.Player;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
/** public final class MineActionHandler {
* Admin commands for mine management.
* Provides commands for creating, deleting, resetting, reloading, listing, and editing mines.
*/
@Command(value = "lm", permission = "lomines.use")
public class AdminCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Mines mines; private final Mines mines;
private final ErrorHandler errorHandler; private final ErrorHandler errorHandler;
public AdminCommands(LoMinesPlugin plugin) { public MineActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) {
this.plugin = plugin; this.plugin = plugin;
this.mines = plugin.getMines(); this.mines = mines;
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); this.errorHandler = errorHandler;
} }
/** public void create(CommandSender sender, String name) {
* Creates a new mine with the given name.
* Usage: /lm create <name>
*/
@Subcommand(value = "create", permission = "lomines.admin.create")
public void create(CommandSender sender, @Arg("name") String name) {
try { try {
mines.create(name); mines.create(name);
sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name)); sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name));
@ -58,12 +44,7 @@ public class AdminCommands {
} }
} }
/** public void delete(CommandSender sender, String name) {
* Deletes an existing mine.
* Usage: /lm delete <name>
*/
@Subcommand(value = "delete", permission = "lomines.admin.delete")
public void delete(CommandSender sender, @Arg("name") String name) {
try { try {
mines.delete(name); mines.delete(name);
sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name)); sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name));
@ -75,12 +56,7 @@ public class AdminCommands {
} }
} }
/** public void edit(CommandSender sender, String mineName) {
* Opens the GUI editor for a mine.
* Usage: /lm edit <mine>
*/
@Subcommand(value = "edit", permission = "lomines.admin.edit")
public void edit(CommandSender sender, @Arg("mine") String mineName) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return; return;
@ -95,16 +71,7 @@ public class AdminCommands {
MineEditGui.open(plugin, player, mineName); MineEditGui.open(plugin, player, mineName);
} }
/** public void reset(CommandSender sender, String mineName, boolean silent) {
* Resets a mine by refilling all its regions.
* Usage: /lm reset <mine> [silent]
*/
@Subcommand(value = "reset", permission = "lomines.admin.reset")
public void reset(
CommandSender sender,
@Arg("mine") String mineName,
@Arg(value = "silent", optional = true, defaultValue = "false") boolean silent
) {
try { try {
Mine mine = mines.get(mineName); Mine mine = mines.get(mineName);
mine.reset(silent); mine.reset(silent);
@ -114,11 +81,6 @@ public class AdminCommands {
} }
} }
/**
* Reloads all mine configurations from disk.
* Usage: /lm reload
*/
@Subcommand(value = "reload", permission = "lomines.admin.reload")
public void reload(CommandSender sender) { public void reload(CommandSender sender) {
try { try {
mines.getAll().forEach(Mine::stop); mines.getAll().forEach(Mine::stop);
@ -130,32 +92,4 @@ public class AdminCommands {
"Failed to reload mines", e); "Failed to reload mines", e);
} }
} }
/**
* Lists all active mines.
* Usage: /lm list
*/
@Subcommand(value = "list", permission = "lomines.admin.list")
public void list(CommandSender sender) {
Collection<Mine> allMines = mines.getAll();
if (allMines.isEmpty()) {
sender.sendMessage(Component.text("No mines found"));
return;
}
sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ==="));
for (Mine mine : allMines) {
String status = formatMineStatus(mine);
sender.sendMessage(Component.text("- " + mine.getName() + " " + status));
}
}
private String formatMineStatus(Mine mine) {
int blocks = mine.getBlocks();
int total = mine.getTotalVolume();
double percent = mine.getPercentFilled();
return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent);
}
} }

View file

@ -1,23 +1,16 @@
package dev.loki.lomines.command.admin; package dev.loki.lomines.command.admin.misc;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.core.mine.registry.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.Command; import org.bukkit.Bukkit;
import dev.lolib.commands.annotation.Subcommand;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import java.io.IOException; import java.io.IOException;
/**
* Mask commands for mine shape management.
* Provides commands for scanning and saving mask positions.
*/
@Command(value = "lm", permission = "lomines.use")
public class MaskCommands { public class MaskCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
@ -30,9 +23,13 @@ public class MaskCommands {
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
} }
@Subcommand(value = "maskscan", permission = "lomines.admin.maskscan") public void handle(CommandSender sender, String[] args) {
public void maskScan(CommandSender sender, @Arg("mine") String name) { if (args.length < 1) {
Scheduler.get(plugin).run(() -> { sender.sendMessage(MessageFormatter.error("Usage: /lm maskscan <mine>"));
return;
}
String name = args[0];
Bukkit.getScheduler().runTask(plugin, () -> {
try { try {
int n = mines.scanAndSaveMask(name); int n = mines.scanAndSaveMask(name);
sender.sendMessage(MessageFormatter.success( sender.sendMessage(MessageFormatter.success(

View file

@ -1,42 +1,32 @@
package dev.loki.lomines.command.admin; package dev.loki.lomines.command.admin.misc;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig;
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.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;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
/** public final class TeleportActionHandler {
* Commands for managing teleport and spawn locations for mines.
*/
class TeleportCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private final Mines mines; private final Mines mines;
private final ErrorHandler errorHandler; private final ErrorHandler errorHandler;
TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { public TeleportActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) {
this.plugin = plugin; this.plugin = plugin;
this.mines = mines; this.mines = mines;
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
} }
/** public void setTeleport(CommandSender sender, String mineName) {
* Sets the teleport location for players when mine resets.
* Usage: /lm setteleport <mine>
*/
@Subcommand(value = "setteleport", permission = "lomines.admin.setteleport")
void setTeleport(CommandSender sender, @Arg("mine") String mineName) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return; return;
@ -50,18 +40,7 @@ class TeleportCommands {
} }
var config = mine.getConfig(); var config = mine.getConfig();
var newTeleport = TeleportConfig.at(player.getLocation()); var updatedConfig = withTeleport(config, TeleportConfig.at(player.getLocation()));
var updatedConfig = MineConfig.builder(mineName)
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(newTeleport)
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
plugin.getMines().getFileManager().saveConfig(updatedConfig); plugin.getMines().getFileManager().saveConfig(updatedConfig);
sender.sendMessage(MessageFormatter.success("Teleport location for mine '" + mineName + "' set to your current position")); sender.sendMessage(MessageFormatter.success("Teleport location for mine '" + mineName + "' set to your current position"));
@ -72,12 +51,7 @@ class TeleportCommands {
} }
} }
/** public void setSpawn(CommandSender sender, String mineName) {
* Sets the safe spawn location for stuck players after mine reset.
* Usage: /lm setspawn <mine>
*/
@Subcommand(value = "setspawn", permission = "lomines.admin.setspawn")
void setSpawn(CommandSender sender, @Arg("mine") String mineName) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return; return;
@ -91,18 +65,7 @@ class TeleportCommands {
} }
var config = mine.getConfig(); var config = mine.getConfig();
var newSpawn = PlayerSpawnConfig.at(player.getLocation()); var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.at(player.getLocation()));
var updatedConfig = MineConfig.builder(mineName)
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(newSpawn)
.build();
plugin.getMines().getFileManager().saveConfig(updatedConfig); plugin.getMines().getFileManager().saveConfig(updatedConfig);
sender.sendMessage(MessageFormatter.success("Safe spawn location for mine '" + mineName + "' set to your current position")); sender.sendMessage(MessageFormatter.success("Safe spawn location for mine '" + mineName + "' set to your current position"));
@ -114,12 +77,7 @@ class TeleportCommands {
} }
} }
/** public void clearSpawn(CommandSender sender, String mineName) {
* Clears the spawn location for stuck players.
* Usage: /lm clearspawn <mine>
*/
@Subcommand(value = "clearspawn", permission = "lomines.admin.setspawn")
void clearSpawn(CommandSender sender, @Arg("mine") String mineName) {
try { try {
Mine mine = mines.find(mineName).orElse(null); Mine mine = mines.find(mineName).orElse(null);
if (mine == null) { if (mine == null) {
@ -128,16 +86,7 @@ class TeleportCommands {
} }
var config = mine.getConfig(); var config = mine.getConfig();
var updatedConfig = MineConfig.builder(mineName) var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.disabled());
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(PlayerSpawnConfig.disabled())
.build();
plugin.getMines().getFileManager().saveConfig(updatedConfig); plugin.getMines().getFileManager().saveConfig(updatedConfig);
sender.sendMessage(MessageFormatter.success("Spawn location for mine '" + mineName + "' cleared")); sender.sendMessage(MessageFormatter.success("Spawn location for mine '" + mineName + "' cleared"));
@ -148,7 +97,33 @@ class TeleportCommands {
} }
} }
private String formatLocation(Location loc) { private MineConfig withTeleport(MineConfig config, TeleportConfig teleport) {
return MineConfig.builder(config.name())
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(teleport)
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
}
private MineConfig withPlayerSpawn(MineConfig config, PlayerSpawnConfig spawn) {
return MineConfig.builder(config.name())
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(spawn)
.build();
}
public String formatLocation(Location loc) {
return String.format("%.1f, %.1f, %.1f in %s", return String.format("%.1f, %.1f, %.1f in %s",
loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName()); loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName());
} }

View file

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

View file

@ -1,35 +1,30 @@
package dev.loki.lomines.command.admin; package dev.loki.lomines.command.admin.stats;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.integration.hologram.HologramManager; import dev.loki.lomines.integration.hologram.HologramManager;
import dev.lolilb.commands.annotation.Arg;
import dev.lolilb.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;
/** public class HologramCommands {
* Commands for managing holograms above mines.
*/
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();
} }
/** public void handle(CommandSender sender, String[] args) {
* Toggles hologram for a mine. if (args.length < 1) {
* Usage: /lm hologram <mine> [on|off] sender.sendMessage(Component.text("Usage: /lm hologram <mine> [on|off]", NamedTextColor.RED));
*/ return;
@Subcommand(value = "hologram", permission = "lomines.admin.hologram") }
void hologram(CommandSender sender, @Arg("mine") String mineName, String mineName = args[0];
@Arg(value = "state", optional = true) String state) { String state = args.length > 1 ? args[1] : null;
Mine mine = plugin.getMines().find(mineName).orElse(null); Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) { if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
@ -43,10 +38,9 @@ class HologramCommands {
boolean enable; boolean enable;
if (state == null) { if (state == null) {
// Toggle
enable = hologramManager.toggleHologram(mine); enable = hologramManager.toggleHologram(mine);
} else { } else {
enable = state.equalsIgnoreCase("on") || state.equalsIgnoreCase("true") || state.equalsIgnoreCase("enable"); enable = "on".equalsIgnoreCase(state) || "true".equalsIgnoreCase(state) || "enable".equalsIgnoreCase(state);
if (enable) { if (enable) {
hologramManager.createMineHologram(mine); hologramManager.createMineHologram(mine);
} else { } else {
@ -55,6 +49,6 @@ class HologramCommands {
} }
sender.sendMessage(Component.text( sender.sendMessage(Component.text(
"Голограмма для шахты " + mineName + " " + (enable ? "§aвключена" : "§cвыключена"))); "Голограмма для шахты " + mineName + " " + (enable ? "§aвключена" : "§cвыключена")));
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,63 @@
package dev.loki.lomines.command.player;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class TeleportCommand {
private final LoMinesPlugin plugin;
public TeleportCommand(LoMinesPlugin plugin) {
this.plugin = plugin;
}
public void handle(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm tp <mine>", NamedTextColor.RED));
return;
}
String mineName = args[0];
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED));
return;
}
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
Location target = resolveTarget(player, mine);
if (target == null) {
return;
}
player.teleport(target);
player.sendMessage(Component.text("§aТелепортировано к шахте §f" + mine.getName() +
" §a(§f" + String.format("%.0f, %.0f, %.0f", target.getX(), target.getY(), target.getZ()) + "§a)"));
}
private Location resolveTarget(Player player, Mine mine) {
var config = mine.getConfig();
if (config.teleport().enabled() && config.teleport().getLocation().isPresent()) {
return config.teleport().getLocation().get();
}
if (!mine.getRegions().isEmpty()) {
Cuboid region = mine.getRegions().get(0);
double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5;
double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5;
double centerY = region.getMaxY() + 1;
return new Location(region.getWorld(), centerX, centerY, centerZ);
}
player.sendMessage(Component.text("У шахты нет настроенной точки телепорта!", NamedTextColor.RED));
return null;
}
}

View file

@ -1,11 +0,0 @@
/**
* Core components of the LoMines plugin.
* <p>
* This package contains the main plugin class and core system classes:
* <ul>
* <li>{@code LoMinesPlugin} - Main plugin class extending LoPlugin</li>
* <li>{@code Mines} - Registry for managing all active mines</li>
* <li>{@code Mine} - Representation of a single mine with its configuration and state</li>
* </ul>
*/
package dev.loki.lomines.core.common;

View file

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

View file

@ -0,0 +1,82 @@
package dev.loki.lomines.core.mine.model;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.util.location.BlockKeys;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public final class MineState {
private final AtomicInteger blocks;
private final AtomicInteger ticks;
private final int totalVolume;
private final Set<String> maskBlockKeys;
public MineState(MineConfig config) {
this.maskBlockKeys = initMaskBlockKeys(config);
this.totalVolume = config.blocks().fillMode() == FillMode.MASK
? maskBlockKeys.size()
: config.region().totalVolume();
this.blocks = new AtomicInteger(Math.max(0, totalVolume));
this.ticks = new AtomicInteger(0);
}
private static Set<String> initMaskBlockKeys(MineConfig config) {
if (config.blocks().fillMode() != FillMode.MASK) {
return Set.of();
}
Set<String> keys = new HashSet<>();
if (config.blocks().mask() != null) {
for (String pos : config.blocks().mask().positions().keySet()) {
Location loc = LocationParser.parseSimple(pos);
if (loc != null) {
keys.add(BlockKeys.key(loc));
}
}
}
return keys;
}
public int getTotalVolume() {
return totalVolume;
}
public double getPercentFilled() {
if (totalVolume == 0) {
return 0.0;
}
return (double) blocks.get() / totalVolume * 100.0;
}
public AtomicInteger getBlocksAtomic() {
return blocks;
}
public int getBlocks() {
return blocks.get();
}
public int getTicks() {
return ticks.get();
}
public AtomicInteger getTicksAtomic() {
return ticks;
}
public Set<String> getMaskBlockKeys() {
return maskBlockKeys;
}
public boolean appliesToBlock(Location location) {
if (maskBlockKeys.isEmpty()) {
return true;
}
return maskBlockKeys.contains(BlockKeys.key(location));
}
}

View file

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

View file

@ -1,23 +1,20 @@
package dev.loki.lomines.core.mine; package dev.loki.lomines.core.mine.registry;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.common.MineLoader; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.service.MaskScanService; import dev.loki.lomines.core.mine.service.MineLoader;
import dev.loki.lomines.core.service.mask.MaskScanService;
import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.core.service.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.model.MineConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardRegionService; import dev.loki.lomines.integration.worldguard.region.WorldGuardRegionService;
import org.bukkit.Location; import org.bukkit.Location;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
/**
* Registry for all active mines.
* Manages the lifecycle of mines including loading, creating, and deleting.
*/
public final class Mines { public final class Mines {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
@ -26,6 +23,7 @@ public final class Mines {
private final MineLoader loader; private final MineLoader loader;
private final MaskScanService maskScanService; private final MaskScanService maskScanService;
private final WorldGuardRegionService worldGuardService; private final WorldGuardRegionService worldGuardService;
private final MineFinder finder;
public Mines(LoMinesPlugin plugin) { public Mines(LoMinesPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
@ -34,6 +32,7 @@ public final class Mines {
this.loader = new MineLoader(plugin, fileManager, repository); this.loader = new MineLoader(plugin, fileManager, repository);
this.maskScanService = new MaskScanService(repository, fileManager); this.maskScanService = new MaskScanService(repository, fileManager);
this.worldGuardService = new WorldGuardRegionService(plugin); this.worldGuardService = new WorldGuardRegionService(plugin);
this.finder = new MineFinder(repository);
} }
public void loadAll() throws IOException { public void loadAll() throws IOException {
@ -45,6 +44,7 @@ public final class Mines {
} }
public void create(String name, Location corner1, Location corner2) throws IOException { public void create(String name, Location corner1, Location corner2) throws IOException {
name = name.toLowerCase().trim();
if (repository.exists(name)) { if (repository.exists(name)) {
throw new IllegalArgumentException("Mine already exists: " + name); throw new IllegalArgumentException("Mine already exists: " + name);
} }
@ -64,7 +64,7 @@ public final class Mines {
worldGuardService.createRegion(name, config); worldGuardService.createRegion(name, config);
repository.createAndStart(name, config); repository.createAndStart(name, config);
plugin.loLogger().info("Created mine: " + name); plugin.getLogger().info("Created mine: " + name);
} }
public void delete(String name) throws IOException { public void delete(String name) throws IOException {
@ -79,39 +79,65 @@ public final class Mines {
} }
public Mine get(String name) { public Mine get(String name) {
return repository.get(name); return finder.get(name);
} }
public Optional<Mine> find(String name) { public Optional<Mine> find(String name) {
return repository.find(name); return finder.find(name);
} }
public Collection<Mine> getAll() { public Collection<Mine> getAll() {
return repository.getAll(); return finder.getAll();
} }
public Optional<Mine> findByLocation(Location location) { public Optional<Mine> findByLocation(Location location) {
return repository.findByLocation(location); return finder.findByLocation(location);
} }
public int scanAndSaveMask(String mineName) throws IOException, ConfigLoader.ConfigLoadException { public int scanAndSaveMask(String mineName) throws IOException, ConfigLoader.ConfigLoadException {
int count = maskScanService.scanAndSave(mineName); int count = maskScanService.scanAndSave(mineName);
plugin.loLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)"); plugin.getLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)");
return count; return count;
} }
public void reloadMine(String name) throws IOException, ConfigLoader.ConfigLoadException { public void reloadMine(String name) throws IOException, ConfigLoader.ConfigLoadException {
Mine oldMine = repository.find(name).orElse(null); Mine oldMine = finder.find(name).orElse(null);
repository.reload(name); repository.reload(name);
if (oldMine != null) { if (oldMine != null) {
Mine newMine = repository.find(name).orElse(null); Mine newMine = finder.find(name).orElse(null);
if (newMine != null) { if (newMine != null) {
worldGuardService.updateRegion(name, newMine.getConfig()); worldGuardService.updateRegion(name, newMine.getConfig());
} }
} }
} }
public void updateMineConfig(String name, MineConfig newConfig) throws IOException {
if (!repository.exists(name)) {
throw new IllegalArgumentException("Mine not found: " + name);
}
try {
fileManager.saveConfig(name, newConfig);
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save mine config: " + e.getMessage(), e);
}
try {
Mine oldMine = finder.find(name).orElse(null);
repository.reload(name);
if (oldMine != null) {
Mine newMine = finder.find(name).orElse(null);
if (newMine != null) {
worldGuardService.updateRegion(name, newMine.getConfig());
}
}
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to reload mine after config update: " + e.getMessage(), e);
}
}
public WorldGuardRegionService getWorldGuardService() { public WorldGuardRegionService getWorldGuardService() {
return worldGuardService; return worldGuardService;
} }

View file

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

View file

@ -1,8 +1,10 @@
package dev.loki.lomines.core.mine; package dev.loki.lomines.core.mine.service;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.scheduler.ScheduledTask; import dev.loki.lomines.core.mine.model.Mine;
import dev.lolib.scheduler.Scheduler; import dev.loki.lomines.core.mine.registry.Mines;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
/** /**
* Centralized ticker for all mines. * Centralized ticker for all mines.
@ -13,7 +15,7 @@ public final class MineTicker {
private final Mines mines; private final Mines mines;
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
private ScheduledTask tickerTask; private BukkitTask tickerTask;
public MineTicker(Mines mines, LoMinesPlugin plugin) { public MineTicker(Mines mines, LoMinesPlugin plugin) {
this.mines = mines; this.mines = mines;
@ -29,7 +31,7 @@ public final class MineTicker {
return; // Already running return; // Already running
} }
tickerTask = Scheduler.get(plugin).runTimer(() -> { tickerTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
for (Mine mine : mines.getAll()) { for (Mine mine : mines.getAll()) {
tickMine(mine); tickMine(mine);
} }
@ -52,7 +54,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,38 +1,25 @@
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.core.service.write.MineConfigWriter;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
/** public final class MineFileManager {
* Handles file operations for mine configurations.
* Updated for new section-based configuration (v2). private final Path minesFolder;
*/ private final ConfigLoader configLoader;
public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { private final MineConfigWriter configWriter;
public MineFileManager(Path minesFolder) { public MineFileManager(Path minesFolder) {
this(minesFolder, new ConfigLoader(minesFolder.getParent())); this.minesFolder = minesFolder;
this.configLoader = new ConfigLoader(minesFolder.getParent());
this.configWriter = new MineConfigWriter(minesFolder, configLoader);
} }
public Path getMinesFolder() { public Path getMinesFolder() {
@ -45,82 +32,9 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
} }
} }
/**
* Creates a default mine configuration with new format.
* WorldGuard region settings are loaded from defaults.yml.
*/
public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException { public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException {
ensureFolderExists(); ensureFolderExists();
configWriter.createDefaultConfig(name, corner1, corner2);
// Create default region
Location loc1 = corner1 != null ? corner1 : new Location(
org.bukkit.Bukkit.getWorlds().get(0), 0, 64, 0);
Location loc2 = corner2 != null ? corner2 : new Location(
org.bukkit.Bukkit.getWorlds().get(0), 10, 74, 10);
Cuboid cuboid = new Cuboid(loc1, loc2);
RegionConfig region = RegionConfig.single(cuboid);
// Create default blocks (stone only)
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 1.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
// Load WorldGuard defaults from _defaults.yml
WorldGuardConfig wgConfig = loadWorldGuardDefaults();
// Build mine config with defaults
MineConfig config = MineConfig.builder(name)
.region(region)
.blocks(blocks)
.reset(ResetConfig.defaults())
.rewards(RewardConfig.empty())
.teleport(TeleportConfig.disabled())
.ui(UIConfig.defaults())
.worldGuard(wgConfig)
.playerSpawn(PlayerSpawnConfig.disabled())
.build();
// Save using new loader
configLoader.save(config);
}
/**
* Loads WorldGuard configuration from defaults.yml.
*/
private WorldGuardConfig loadWorldGuardDefaults() {
Path defaultsPath = minesFolder.resolve("_defaults.yml");
if (!Files.exists(defaultsPath)) {
return WorldGuardConfig.disabled();
}
try {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard");
if (wgSection == null) {
return WorldGuardConfig.disabled();
}
boolean enabled = wgSection.getBoolean("enabled", false);
if (!enabled) {
return WorldGuardConfig.disabled();
}
String template = wgSection.getString("region-template", "{mine_name}_{random_4}");
return WorldGuardConfig.builder()
.enabled(true)
.template(template)
.owners(wgSection.getStringList("owners"))
.members(wgSection.getStringList("members"))
.flags(wgSection.getStringList("flags"))
.protectOnCreate(wgSection.getBoolean("protect-on-create", true))
.build();
} catch (Exception e) {
return WorldGuardConfig.disabled();
}
} }
public MineConfig loadConfig(String name) throws IOException, ConfigLoader.ConfigLoadException { public MineConfig loadConfig(String name) throws IOException, ConfigLoader.ConfigLoadException {
@ -134,48 +48,16 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
} }
} }
/**
* Saves a mine configuration to file.
*
* @param config the configuration to save
* @throws ConfigLoader.ConfigLoadException if saving fails
*/
public void saveConfig(MineConfig config) throws ConfigLoader.ConfigLoadException { public void saveConfig(MineConfig config) throws ConfigLoader.ConfigLoadException {
configLoader.save(config); configLoader.save(config);
} }
/** public void saveConfig(String name, MineConfig config) throws ConfigLoader.ConfigLoadException {
* Saves mask positions for mask fill mode. configLoader.save(config);
*/ }
public void saveMaskPositions(String name, BlockKey markerMaterial, java.util.List<Location> positions) throws IOException, ConfigLoader.ConfigLoadException {
MineConfig config = loadConfig(name);
// Build new mask config public void saveMaskPositions(String name, BlockKey markerMaterial,
Map<String, Boolean> posMap = new HashMap<>(); java.util.List<Location> positions) throws IOException, ConfigLoader.ConfigLoadException {
for (Location loc : positions) { configWriter.saveMaskPositions(name, markerMaterial, positions);
posMap.put(LocationParser.format(loc), true);
}
BlockConfig.MaskConfig mask = new BlockConfig.MaskConfig(markerMaterial, posMap);
// Create new block config with mask
BlockConfig newBlocks = new BlockConfig(
config.blocks().weights(),
FillMode.MASK,
mask
);
// Build updated config
MineConfig updated = MineConfig.builder(name)
.region(config.region())
.blocks(newBlocks)
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
configLoader.save(updated);
} }
} }

View file

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

View file

@ -1,10 +1,12 @@
package dev.loki.lomines.core.service; package dev.loki.lomines.core.service.mask;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.core.service.MineRepository;
import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.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.geo.Cuboid;
import dev.loki.lomines.util.selection.MaskScanner; import dev.loki.lomines.util.selection.MaskScanner;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
@ -57,8 +59,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

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

View file

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

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

View file

@ -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

@ -32,7 +32,9 @@ public final class BlockConfigLoader {
Map<BlockKey, Double> weights = new HashMap<>(); Map<BlockKey, Double> weights = new HashMap<>();
for (String key : contents.getKeys(false)) { for (String key : contents.getKeys(false)) {
double weight = contents.getDouble(key); double weight = contents.getDouble(key);
if (weight <= 0) continue; if (weight <= 0) {
continue;
}
weights.put(BlockKey.deserialize(key), weight); weights.put(BlockKey.deserialize(key), weight);
} }
@ -40,22 +42,23 @@ public final class BlockConfigLoader {
yaml.getString("blocks.fill-mode", "CUBOID").toUpperCase() yaml.getString("blocks.fill-mode", "CUBOID").toUpperCase()
); );
// Parse mask config if applicable return new BlockConfig(weights, fillMode, parseMaskConfig(yaml, fillMode));
BlockConfig.MaskConfig mask = null; }
if (fillMode == FillMode.MASK) {
String markerStr = yaml.getString("blocks.mask.marker", "pink_concrete");
BlockKey marker = BlockKey.deserialize(markerStr);
Map<String, Boolean> positions = new HashMap<>(); private BlockConfig.MaskConfig parseMaskConfig(YamlConfiguration yaml, FillMode fillMode) {
List<String> posList = yaml.getStringList("blocks.mask.positions"); if (fillMode != FillMode.MASK) {
for (String pos : posList) { return null;
positions.put(pos, true); }
} String markerStr = yaml.getString("blocks.mask.marker", "pink_concrete");
BlockKey marker = BlockKey.deserialize(markerStr);
mask = new BlockConfig.MaskConfig(marker, positions); Map<String, Boolean> positions = new HashMap<>();
List<String> posList = yaml.getStringList("blocks.mask.positions");
for (String pos : posList) {
positions.put(pos, true);
} }
return new BlockConfig(weights, fillMode, mask); return new BlockConfig.MaskConfig(marker, positions);
} }
/** /**

View file

@ -17,32 +17,17 @@ public final class RegionConfigLoader {
/** /**
* Parses region configuration from YAML. * Parses region configuration from YAML.
* Supports up to 100 selection points (50 regions).
*/ */
public RegionConfig parse(YamlConfiguration yaml) { public RegionConfig parse(YamlConfiguration yaml) {
List<Location> selections = new ArrayList<>(); List<Location> selections = new ArrayList<>();
// Try new format first // Try new format first (region.selection.X)
for (int i = 1; i <= 10; i++) { selections.addAll(parseSelections(yaml, "region.selection.", 100));
String key = "region.selection." + i;
if (yaml.contains(key)) {
String locStr = yaml.getString(key);
if (locStr != null && !locStr.isBlank()) {
selections.add(parseLocation(locStr));
}
}
}
// Legacy fallback // Legacy fallback (selection.X)
if (selections.isEmpty()) { if (selections.isEmpty()) {
for (int i = 1; i <= 10; i++) { selections.addAll(parseSelections(yaml, "selection.", 100));
String key = "selection." + i;
if (yaml.contains(key)) {
String locStr = yaml.getString(key);
if (locStr != null && !locStr.isBlank()) {
selections.add(parseLocation(locStr));
}
}
}
} }
if (selections.size() < 2) { if (selections.size() < 2) {
@ -52,15 +37,56 @@ public final class RegionConfigLoader {
return RegionConfig.fromSelections(selections); return RegionConfig.fromSelections(selections);
} }
/**
* Parses selection points from YAML with given prefix.
* Supports indexed format: prefix.1, prefix.2, etc.
*/
private List<Location> parseSelections(YamlConfiguration yaml, String prefix, int maxPoints) {
List<Location> selections = new ArrayList<>();
for (int i = 1; i <= maxPoints; i++) {
String key = prefix + i;
if (yaml.contains(key)) {
String locStr = yaml.getString(key);
if (locStr != null && !locStr.isBlank()) {
try {
selections.add(parseLocation(locStr));
} catch (IllegalArgumentException e) {
// Skip invalid locations but continue parsing
}
}
} else {
// Stop at first missing index (assuming sequential)
// But check at least a few more for gaps
if (i > 20) {
boolean hasMore = false;
for (int j = i + 1; j <= Math.min(i + 5, maxPoints); j++) {
if (yaml.contains(prefix + j)) {
hasMore = true;
break;
}
}
if (!hasMore) {
break;
}
}
}
}
return selections;
}
/** /**
* Saves region configuration to YAML. * Saves region configuration to YAML.
*/ */
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.config.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.
@ -23,51 +19,23 @@ public final class WorldGuardConfigLoader {
/** /**
* Parses WorldGuard configuration from YAML. * Parses WorldGuard configuration from YAML.
* Supports any WorldGuard flags. Default: block-break=allow.
*/ */
public WorldGuardConfig parse(YamlConfiguration yaml) { public WorldGuardConfig parse(YamlConfiguration yaml) {
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard"); if (!isEnabled(yaml)) {
if (wgSection == null) {
// Check defaults
boolean defaultEnabled = defaults.getBoolean("worldguard.enabled", false);
if (!defaultEnabled) {
return WorldGuardConfig.disabled();
}
}
boolean enabled = yaml.getBoolean("worldguard.enabled",
defaults.getBoolean("worldguard.enabled", false));
if (!enabled) {
return WorldGuardConfig.disabled(); return WorldGuardConfig.disabled();
} }
String template = yaml.getString("worldguard.region-template", List<String> owners = resolveOwners(yaml);
List<String> members = resolveMembers(yaml);
List<String> flags = resolveFlags(yaml);
boolean protect = resolveProtect(yaml);
String tmpl = yaml.getString("worldguard.region-template",
defaults.getString("worldguard.region-template", "{mine_name}_{random_4}")); defaults.getString("worldguard.region-template", "{mine_name}_{random_4}"));
List<String> owners = yaml.getStringList("worldguard.owners");
if (owners.isEmpty()) {
owners = defaults.getStringList("worldguard.owners");
}
List<String> members = yaml.getStringList("worldguard.members");
if (members.isEmpty()) {
members = defaults.getStringList("worldguard.members");
}
List<String> flags = yaml.getStringList("worldguard.flags");
if (flags.isEmpty()) {
flags = defaults.getStringList("worldguard.flags");
}
if (flags.isEmpty()) {
flags = List.of("passthrough=deny", "build=allow");
}
boolean protect = yaml.getBoolean("worldguard.protect-on-create",
defaults.getBoolean("worldguard.protect-on-create", true));
return WorldGuardConfig.builder() return WorldGuardConfig.builder()
.enabled(true) .enabled(true)
.template(template) .template(tmpl)
.owners(owners) .owners(owners)
.members(members) .members(members)
.flags(flags) .flags(flags)
@ -75,6 +43,50 @@ public final class WorldGuardConfigLoader {
.build(); .build();
} }
private boolean isEnabled(YamlConfiguration yaml) {
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard");
if (wgSection == null) {
boolean defaultEnabled = defaults.getBoolean("worldguard.enabled", false);
if (!defaultEnabled) {
return false;
}
}
return yaml.getBoolean("worldguard.enabled", defaults.getBoolean("worldguard.enabled", false));
}
private List<String> resolveOwners(YamlConfiguration yaml) {
List<String> owners = yaml.getStringList("worldguard.owners");
if (owners.isEmpty()) {
owners = defaults.getStringList("worldguard.owners");
}
return owners;
}
private List<String> resolveMembers(YamlConfiguration yaml) {
List<String> members = yaml.getStringList("worldguard.members");
if (members.isEmpty()) {
members = defaults.getStringList("worldguard.members");
}
return members;
}
private List<String> resolveFlags(YamlConfiguration yaml) {
// Support any WorldGuard flags. Default: only block-break=allow
List<String> flags = yaml.getStringList("worldguard.flags");
if (flags.isEmpty()) {
flags = defaults.getStringList("worldguard.flags");
}
if (flags.isEmpty()) {
flags = List.of("block-break=allow");
}
return flags;
}
private boolean resolveProtect(YamlConfiguration yaml) {
return yaml.getBoolean("worldguard.protect-on-create",
defaults.getBoolean("worldguard.protect-on-create", true));
}
/** /**
* Saves WorldGuard configuration to YAML. * Saves WorldGuard configuration to YAML.
*/ */
@ -92,13 +104,14 @@ public final class WorldGuardConfigLoader {
/** /**
* Sets default values for WorldGuard configuration. * Sets default values for WorldGuard configuration.
* Default: only block-break=allow. User can add any flags.
*/ */
public void setDefaults(YamlConfiguration yaml) { public void setDefaults(YamlConfiguration yaml) {
yaml.set("worldguard.enabled", false); yaml.set("worldguard.enabled", false);
yaml.set("worldguard.region-template", "{mine_name}_{random_4}"); yaml.set("worldguard.region-template", "{mine_name}_{random_4}");
yaml.set("worldguard.owners", List.of()); yaml.set("worldguard.owners", List.of());
yaml.set("worldguard.members", List.of()); yaml.set("worldguard.members", List.of());
yaml.set("worldguard.flags", List.of("passthrough=deny", "build=allow")); yaml.set("worldguard.flags", List.of("block-break=allow"));
yaml.set("worldguard.protect-on-create", true); yaml.set("worldguard.protect-on-create", true);
} }
} }

View file

@ -36,18 +36,31 @@ public final class RewardConfigLoader {
} }
private RewardEntry parseRewardEntry(Map<?, ?> map) { private RewardEntry parseRewardEntry(Map<?, ?> map) {
double chance = ((Number) map.getOrDefault("chance", 0)).doubleValue(); List<BlockKey> blocks = parseBlocks(map.get("blocks"));
List<ItemReward> items = parseItems(map.get("items"));
List<String> commands = parseCommands(map.get("commands"));
Object preventDropsObj = map.get("prevent-drops");
boolean preventDrops = preventDropsObj instanceof Boolean b && b;
Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
return new RewardEntry(blocks, chance, items, commands, preventDrops);
}
private List<BlockKey> parseBlocks(Object rawBlocks) {
List<BlockKey> blocks = new ArrayList<>(); List<BlockKey> blocks = new ArrayList<>();
List<?> blockList = (List<?>) map.get("blocks"); List<?> blockList = (List<?>) rawBlocks;
if (blockList != null) { if (blockList != null) {
for (Object b : blockList) { for (Object b : blockList) {
blocks.add(BlockKey.deserialize(b.toString())); blocks.add(BlockKey.deserialize(b.toString()));
} }
} }
return blocks;
}
private List<ItemReward> parseItems(Object rawItems) {
List<ItemReward> items = new ArrayList<>(); List<ItemReward> items = new ArrayList<>();
List<?> itemList = (List<?>) map.get("items"); List<?> itemList = (List<?>) rawItems;
if (itemList != null) { if (itemList != null) {
for (Object i : itemList) { for (Object i : itemList) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -55,12 +68,19 @@ public final class RewardConfigLoader {
items.add(parseItemReward(itemMap)); items.add(parseItemReward(itemMap));
} }
} }
return items;
}
@SuppressWarnings("unchecked") private List<String> parseCommands(Object commandsObj) {
List<String> commands = (List<String>) map.getOrDefault("commands", List.of()); List<String> commands = new ArrayList<>();
boolean preventDrops = (Boolean) map.getOrDefault("prevent-drops", false); if (commandsObj instanceof List<?> list) {
for (Object obj : list) {
return new RewardEntry(blocks, chance, items, commands, preventDrops); if (obj != null) {
commands.add(obj.toString());
}
}
}
return commands;
} }
private ItemReward parseItemReward(Map<String, Object> map) { private ItemReward parseItemReward(Map<String, Object> map) {

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

View file

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

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,6 +1,6 @@
package dev.loki.lomines.data.config.region; package dev.loki.lomines.data.config.region;
import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.geo.Cuboid;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.Collections; import java.util.Collections;

View file

@ -1,13 +1,8 @@
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.
*/
public record ResetConfig( public record ResetConfig(
Duration interval, Duration interval,
double percentTrigger, double percentTrigger,
@ -16,41 +11,34 @@ public record ResetConfig(
String broadcastMessage String broadcastMessage
) { ) {
public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5);
public static final double DEFAULT_PERCENT_TRIGGER = 10.0;
public ResetConfig { public ResetConfig {
// Normalize and validate interval interval = ResetConfigValidator.validateInterval(interval);
if (interval == null || interval.isZero() || interval.isNegative()) { percentTrigger = ResetConfigValidator.validatePercentTrigger(percentTrigger);
interval = DEFAULT_INTERVAL;
}
// Ensure interval is reasonable (1 second to 24 hours)
if (interval.getSeconds() < 1) {
interval = Duration.ofSeconds(1);
} else if (interval.getSeconds() > 86400) {
interval = Duration.ofHours(24);
}
// Validate percent trigger
if (percentTrigger < 0 || percentTrigger > 100) {
percentTrigger = DEFAULT_PERCENT_TRIGGER;
}
commands = commands != null ? List.copyOf(commands) : List.of(); commands = commands != null ? List.copyOf(commands) : List.of();
broadcastMessage = broadcastMessage != null ? broadcastMessage : ""; broadcastMessage = broadcastMessage != null ? broadcastMessage : "";
} }
/** public static final Duration DEFAULT_INTERVAL = ResetConfigValidator.DEFAULT_INTERVAL;
* Interval in ticks (20 ticks = 1 second). public static final double DEFAULT_PERCENT_TRIGGER = ResetConfigValidator.DEFAULT_PERCENT_TRIGGER;
*/
public static ResetConfig defaults() {
return new ResetConfig(
DEFAULT_INTERVAL,
DEFAULT_PERCENT_TRIGGER,
false,
List.of(),
""
);
}
public static Builder builder() {
return new Builder();
}
public long intervalTicks() { public long intervalTicks() {
return interval.getSeconds() * 20; return interval.getSeconds() * 20;
} }
/**
* Human-readable interval string.
*/
public String intervalDisplay() { public String intervalDisplay() {
long seconds = interval.getSeconds(); long seconds = interval.getSeconds();
if (seconds < 60) { if (seconds < 60) {
@ -62,33 +50,10 @@ public record ResetConfig(
} }
} }
/**
* Whether reset should trigger based on percent mined.
*/
public boolean isPercentTriggerEnabled() { public boolean isPercentTriggerEnabled() {
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;
@ -101,13 +66,13 @@ public record ResetConfig(
return this; return this;
} }
public Builder intervalTicks(long ticks) { public Builder interval(String humanReadable) {
this.interval = Duration.ofSeconds(ticks / 20); this.interval = ResetConfigValidator.parseDuration(humanReadable);
return this; return this;
} }
public Builder interval(String humanReadable) { public Builder intervalTicks(long ticks) {
this.interval = parseDuration(humanReadable); this.interval = Duration.ofSeconds(ticks / 20);
return this; return this;
} }
@ -134,25 +99,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

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

View file

@ -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

@ -2,35 +2,36 @@ package dev.loki.lomines.data.config.spawn;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
/** /**
* Configuration for player spawn location when stuck in blocks. * Configuration for player spawn locations when stuck in blocks.
* 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 record PlayerSpawnConfig( public record PlayerSpawnConfig(boolean enabled, List<Location> locations) {
boolean enabled,
Location location
) {
public PlayerSpawnConfig { public PlayerSpawnConfig(boolean enabled, List<Location> locations) {
if (!enabled) { this.enabled = enabled;
location = null; if (!enabled || locations == null) {
} else if (location != null) { this.locations = List.of();
// Defensive copy } else {
location = location.clone(); // Defensive copy with validation
Objects.requireNonNull(location.getWorld(), "Spawn world cannot be null"); List<Location> copy = new ArrayList<>();
for (Location loc : locations) {
if (loc != null) {
Location cloned = loc.clone();
Objects.requireNonNull(cloned.getWorld(), "Spawn world cannot be null");
copy.add(cloned);
}
}
this.locations = List.copyOf(copy);
} }
} }
/**
* Returns the spawn location if enabled.
*/
public Optional<Location> getLocation() {
return Optional.ofNullable(enabled ? location : null);
}
/** /**
* Disabled spawn config (default). * Disabled spawn config (default).
*/ */
@ -39,9 +40,74 @@ public record PlayerSpawnConfig(
} }
/** /**
* Creates enabled config with location. * Creates enabled config with single location.
*/ */
public static PlayerSpawnConfig at(Location location) { public static PlayerSpawnConfig at(Location location) {
return new PlayerSpawnConfig(true, location); return new PlayerSpawnConfig(true, List.of(location));
}
/**
* Creates enabled config with multiple locations.
*/
public static PlayerSpawnConfig at(List<Location> 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.
*/
public PlayerSpawnConfig addLocation(Location location) {
List<Location> newList = new ArrayList<>(locations);
newList.add(location);
return new PlayerSpawnConfig(true, newList);
}
/**
* Creates a new config without the location at given index.
*/
public PlayerSpawnConfig removeLocation(int index) {
if (index < 0 || index >= locations.size()) {
return this;
}
List<Location> newList = new ArrayList<>(locations);
newList.remove(index);
return new PlayerSpawnConfig(!newList.isEmpty(), newList);
}
/**
* Clears all locations (disables spawn).
*/
public PlayerSpawnConfig clear() {
return disabled();
} }
} }

View file

@ -2,34 +2,35 @@ package dev.loki.lomines.data.config.teleport;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
/** /**
* Configuration for mine teleportation on reset. * Configuration for mine teleportation on reset.
* Supports multiple teleport locations for a single mine.
*/ */
public record TeleportConfig( public record TeleportConfig(boolean enabled, List<Location> locations) {
boolean enabled,
Location location
) {
public TeleportConfig { public TeleportConfig(boolean enabled, List<Location> locations) {
if (!enabled) { this.enabled = enabled;
location = null; if (!enabled || locations == null) {
} else if (location != null) { this.locations = List.of();
// Defensive copy } else {
location = location.clone(); // Defensive copy with validation
Objects.requireNonNull(location.getWorld(), "Teleport world cannot be null"); List<Location> copy = new ArrayList<>();
for (Location loc : locations) {
if (loc != null) {
Location cloned = loc.clone();
Objects.requireNonNull(cloned.getWorld(), "Teleport world cannot be null");
copy.add(cloned);
}
}
this.locations = List.copyOf(copy);
} }
} }
/**
* Returns the teleport location if enabled.
*/
public Optional<Location> getLocation() {
return Optional.ofNullable(enabled ? location : null);
}
/** /**
* Disabled teleport config. * Disabled teleport config.
*/ */
@ -38,9 +39,74 @@ public record TeleportConfig(
} }
/** /**
* Creates enabled config with location. * Creates enabled config with single location.
*/ */
public static TeleportConfig at(Location location) { public static TeleportConfig at(Location location) {
return new TeleportConfig(true, location); return new TeleportConfig(true, List.of(location));
}
/**
* Creates enabled config with multiple locations.
*/
public static TeleportConfig at(List<Location> 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.
*/
public TeleportConfig addLocation(Location location) {
List<Location> newList = new ArrayList<>(locations);
newList.add(location);
return new TeleportConfig(true, newList);
}
/**
* Creates a new config without the location at given index.
*/
public TeleportConfig removeLocation(int index) {
if (index < 0 || index >= locations.size()) {
return this;
}
List<Location> newList = new ArrayList<>(locations);
newList.remove(index);
return new TeleportConfig(!newList.isEmpty(), newList);
}
/**
* Clears all locations (disables teleport).
*/
public TeleportConfig clear() {
return disabled();
} }
} }

View file

@ -6,8 +6,8 @@ import java.util.List;
* Configuration for holograms displayed above mines. * Configuration for holograms displayed above mines.
* *
* @param enabled whether holograms are enabled * @param enabled whether holograms are enabled
* @param format list of lines with placeholders {mine}, {percent}, {bar}, {time} * @param format list of lines with placeholders {mine}, {percent}, {bar}, {time}
* @param height offset above the mine region (in blocks) * @param height offset above the mine region (in blocks)
*/ */
public record HologramConfig( public record HologramConfig(
boolean enabled, boolean enabled,

View file

@ -1,9 +1,9 @@
package dev.loki.lomines.data.config.ui; package dev.loki.lomines.data.config.ui;
import net.kyori.adventure.text.minimessage.MiniMessage; import dev.loki.lomines.util.format.color.ColorUtils;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import java.util.Objects; import java.util.Locale;
/** /**
* Configuration for UI elements (action bar, messages). * Configuration for UI elements (action bar, messages).
@ -16,14 +16,11 @@ 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,19 +30,34 @@ 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}
* Supports colors: &#RRGGBB, &a..&f, <color>
*/ */
public Component formatActionBar(String mineName, double percent, String timeStr, int blocks, int total) { public Component formatActionBar(String mineName, double percent, String timeStr, int blocks, int total) {
String parsed = actionBarFormat String parsed = actionBarFormat
.replace("{mine}", mineName) .replace("{mine}", mineName)
.replace("{percent}", String.format("%.1f", percent)) .replace("{percent}", String.format(Locale.US, "%.1f", percent))
.replace("{time}", timeStr) .replace("{time}", timeStr)
.replace("{blocks}", String.valueOf(blocks)) .replace("{blocks}", String.valueOf(blocks))
.replace("{total}", String.valueOf(total)); .replace("{total}", String.valueOf(total));
return MiniMessage.miniMessage().deserialize(parsed); return ColorUtils.format(parsed);
} }
/** /**
@ -70,18 +82,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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,21 @@ 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

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

View file

@ -28,8 +28,8 @@ public final class ConfirmDeleteGui {
/** /**
* Opens the confirmation GUI for deleting a mine. * Opens the confirmation GUI for deleting a mine.
* *
* @param plugin the plugin instance * @param plugin the plugin instance
* @param player the player to open for * @param player the player to open for
* @param mineName the name of the mine to delete * @param mineName the name of the mine to delete
*/ */
public static void open(LoMinesPlugin plugin, Player player, String mineName) { public static void open(LoMinesPlugin plugin, Player player, String mineName) {

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.gui.group; package dev.loki.lomines.gui.group;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.wand.group.GroupWandManager; import dev.loki.lomines.wand.group.GroupWandManager;
import dev.loki.lomines.wand.group.GroupWandSession; import dev.loki.lomines.wand.group.GroupWandSession;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -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());
@ -93,12 +94,28 @@ public final class GroupCreateGui {
return; return;
} }
CreationResult result = createSlots(plugin, player, session, base);
if (result.created() == 0) {
player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED));
return;
}
player.sendMessage(Component.text(
"Создано шахт: " + result.created() + (result.skipped() > 0 ? " (пропущено: " + result.skipped() + ")" : ""),
NamedTextColor.GREEN));
}
private record CreationResult(int created, int skipped) {
}
private static CreationResult createSlots(LoMinesPlugin plugin, Player player, GroupWandSession session, String base) {
Mines mines = plugin.getMines(); Mines mines = plugin.getMines();
int created = 0; int created = 0;
int skipped = 0; int skipped = 0;
for (int i = 0; i < 9; i++) { for (int i = 0; i < 9; i++) {
if (!session.isSlotReady(i)) continue; if (!session.isSlotReady(i)) {
continue;
}
String mineName = base + "_" + (i + 1); String mineName = base + "_" + (i + 1);
try { try {
mines.create(mineName, session.getPos1(i), session.getPos2(i)); mines.create(mineName, session.getPos1(i), session.getPos2(i));
@ -108,16 +125,10 @@ public final class GroupCreateGui {
skipped++; skipped++;
} catch (Exception e) { } catch (Exception e) {
player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED)); player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED));
plugin.loLogger().error("Group create failed for " + mineName, e); plugin.getLogger().severe("Group create failed for " + mineName);
e.printStackTrace();
} }
} }
return new CreationResult(created, skipped);
if (created == 0) {
player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED));
return;
}
player.sendMessage(Component.text(
"Создано шахт: " + created + (skipped > 0 ? " (пропущено: " + skipped + ")" : ""),
NamedTextColor.GREEN));
} }
} }

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

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

View file

@ -1,82 +0,0 @@
package dev.loki.lomines.gui.mine.edit;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
/**
* Item factory for ResetGui.
*/
final class ResetGuiItems {
private ResetGuiItems() {}
static ItemStack intervalItem(ResetConfig config) {
return ItemStackFactory.create(Material.CLOCK, "§a§lИнтервал сброса",
"§8───────────────",
"§7Текущее: §f" + config.intervalDisplay(),
"",
"§e▸ ЛКМ §7+1 минута",
"§e▸ ПКМ §7-1 минута",
"§e▸ Shift+ЛКМ §7+10 минут",
"§e▸ Shift+ПКМ §7-10 минут"
);
}
static ItemStack percentTriggerItem(ResetConfig config) {
String status = config.isPercentTriggerEnabled() ? "§aвкл" : "§7выкл";
String value = config.isPercentTriggerEnabled() ? config.percentTrigger() + "%" : "—";
return ItemStackFactory.create(Material.COMPARATOR, "§a§lТриггер по %",
"§8───────────────",
"§7Статус: " + status,
"§7Значение: §f" + value,
"",
"§7Сброс шахты когда",
"§7добыто указанный %",
"",
"§e▸ ЛКМ §7вкл/выкл",
"§e▸ ПКМ §7изменить %"
);
}
static ItemStack messageItem(ResetConfig config) {
String message = config.broadcastMessage().isEmpty() ? "§7(не задано)" : "§f" + config.broadcastMessage();
return ItemStackFactory.create(Material.BOOK, "§a§lСообщение при сбросе",
"§8───────────────",
"§7Текущее:",
" " + ItemStackFactory.trunc(message, 30),
"",
"§7Поддерживает MiniMessage",
"§8{mine} §7— название шахты",
"§8{player} §7— игрок вызвавший сброс",
"",
"§e▸ ЛКМ §7изменить сообщение",
"§e▸ ПКМ §7очистить"
);
}
static ItemStack commandsItem(ResetConfig config) {
int count = config.commands().size();
return ItemStackFactory.create(Material.COMMAND_BLOCK, "§a§lКоманды при сбросе",
"§8───────────────",
"§7Количество: §f" + count,
"",
"§7Плейсхолдеры:",
"§8{mine} §7— название шахты",
"§8{world} §7— мир",
"",
"§e▸ ЛКМ §7редактировать список"
);
}
static ItemStack backItem() {
return ItemStackFactory.create(Material.ARROW, "§c§lНазад",
"§8───────────────",
"§7Вернуться в редактор шахты",
"",
"§e▸ Нажмите для возврата"
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.gui.mine.edit; package dev.loki.lomines.gui.mine.edit.blocks.view;
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;
@ -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);
@ -18,40 +19,40 @@ final class BlocksGuiItems {
double percent = weight * 100.0; double percent = weight * 100.0;
return ItemStackFactory.create(material, "§a§l" + name, return ItemStackFactory.create(material, "§a§l" + name,
"§8───────────────", "§8───────────────",
"§7Вес: §f" + String.format("%.1f%%", percent), "§7Вес: §f" + String.format("%.1f%%", percent),
"", "",
"§e▸ ЛКМ §7+5%", "§e▸ ЛКМ §7+5%",
"§e▸ ПКМ §7-5%", "§e▸ ПКМ §7-5%",
"§e▸ Shift+ЛКМ §7+1%", "§e▸ Shift+ЛКМ §7+1%",
"§e▸ Shift+ПКМ §7-1% §8(удалить если 0%)", "§e▸ Shift+ПКМ §7-1% §8(удалить если 0%)",
"", "",
"§8ID: §7" + key.serialize() "§8ID: §7" + key.serialize()
); );
} }
static ItemStack addBlockItem() { static ItemStack addBlockItem() {
return ItemStackFactory.create(Material.EMERALD_BLOCK, "§a§lДобавить блок", return ItemStackFactory.create(Material.EMERALD_BLOCK, "§a§lДобавить блок",
"§8───────────────", "§8───────────────",
"§7Добавить новый блок", "§7Добавить новый блок",
"§7в конфигурацию", "§7в конфигурацию",
"", "",
"§e▸ Нажмите для выбора материала" "§e▸ Нажмите для выбора материала"
); );
} }
static ItemStack backItem() { static ItemStack backItem() {
return ItemStackFactory.create(Material.ARROW, "§c§lНазад", return ItemStackFactory.create(Material.ARROW, "§c§lНазад",
"§8───────────────", "§8───────────────",
"§7Вернуться в редактор шахты", "§7Вернуться в редактор шахты",
"", "",
"§e▸ Нажмите для возврата" "§e▸ Нажмите для возврата"
); );
} }
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;
@ -80,8 +81,8 @@ final class BlocksGuiItems {
for (String word : input.split(" ")) { for (String word : input.split(" ")) {
if (!word.isEmpty()) { if (!word.isEmpty()) {
result.append(Character.toUpperCase(word.charAt(0))) result.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1)) .append(word.substring(1))
.append(" "); .append(" ");
} }
} }
return result.toString().trim(); return result.toString().trim();

View file

@ -1,9 +1,9 @@
package dev.loki.lomines.gui.mine.edit; package dev.loki.lomines.gui.mine.edit.reset;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.gui.common.ItemStackFactory; import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.holder.ResetGuiHolder; import dev.loki.lomines.gui.mine.holder.edit.reset.ResetGuiHolder;
import dev.loki.lomines.gui.mine.main.MineEditGui; import 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;
@ -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);
@ -64,9 +65,11 @@ public final class ResetGui {
} }
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot,
String mineName, boolean leftClick, boolean rightClick, String mineName, boolean leftClick, boolean rightClick,
boolean shiftClick) { boolean shiftClick) {
if (rawSlot < 0 || rawSlot >= SIZE) return false; if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
if (rawSlot == SLOT_BACK) { if (rawSlot == SLOT_BACK) {
MineEditGui.open(plugin, player, mineName); MineEditGui.open(plugin, player, mineName);
@ -84,14 +87,7 @@ public final class ResetGui {
} }
if (rawSlot == SLOT_MESSAGE) { if (rawSlot == SLOT_MESSAGE) {
if (leftClick) { return handleMessageSlot(plugin, player, mineName, leftClick, rightClick);
player.sendMessage(Component.text("§eВведите сообщение в чат (или 'cancel' для отмены):"));
player.closeInventory();
} else if (rightClick) {
player.sendMessage(Component.text("§cСообщение очищено."));
refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
return true;
} }
if (rawSlot == SLOT_COMMANDS && leftClick) { if (rawSlot == SLOT_COMMANDS && leftClick) {
@ -102,6 +98,18 @@ public final class ResetGui {
return true; return true;
} }
private static boolean handleMessageSlot(LoMinesPlugin plugin, Player player, String mineName,
boolean leftClick, boolean rightClick) {
if (leftClick) {
player.sendMessage(Component.text("§eВведите сообщение в чат (или 'cancel' для отмены):"));
player.closeInventory();
} else if (rightClick) {
player.sendMessage(Component.text("§cСообщение очищено."));
refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
return true;
}
private static void adjustInterval(LoMinesPlugin plugin, Player player, String mineName, private static void adjustInterval(LoMinesPlugin plugin, Player player, String mineName,
boolean leftClick, boolean rightClick, boolean shiftClick) { boolean leftClick, boolean rightClick, boolean shiftClick) {
int delta = 0; int delta = 0;
@ -122,7 +130,7 @@ public final class ResetGui {
} }
private static void togglePercentTrigger(LoMinesPlugin plugin, Player player, private static void togglePercentTrigger(LoMinesPlugin plugin, Player player,
String mineName, boolean leftClick, boolean rightClick) { String mineName, boolean leftClick, boolean rightClick) {
if (leftClick) { if (leftClick) {
player.sendMessage(Component.text("§aТриггер по % переключен.")); player.sendMessage(Component.text("§aТриггер по % переключен."));
} else if (rightClick) { } else if (rightClick) {

View file

@ -0,0 +1,83 @@
package dev.loki.lomines.gui.mine.edit.reset;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
/**
* Item factory for ResetGui.
*/
final class ResetGuiItems {
private ResetGuiItems() {
}
static ItemStack intervalItem(ResetConfig config) {
return ItemStackFactory.create(Material.CLOCK, "§a§lИнтервал сброса",
"§8───────────────",
"§7Текущее: §f" + config.intervalDisplay(),
"",
"§e▸ ЛКМ §7+1 минута",
"§e▸ ПКМ §7-1 минута",
"§e▸ Shift+ЛКМ §7+10 минут",
"§e▸ Shift+ПКМ §7-10 минут"
);
}
static ItemStack percentTriggerItem(ResetConfig config) {
String status = config.isPercentTriggerEnabled() ? "§aвкл" : "§7выкл";
String value = config.isPercentTriggerEnabled() ? config.percentTrigger() + "%" : "—";
return ItemStackFactory.create(Material.COMPARATOR, "§a§lТриггер по %",
"§8───────────────",
"§7Статус: " + status,
"§7Значение: §f" + value,
"",
"§7Сброс шахты когда",
"§7добыто указанный %",
"",
"§e▸ ЛКМ §7вкл/выкл",
"§e▸ ПКМ §7изменить %"
);
}
static ItemStack messageItem(ResetConfig config) {
String message = config.broadcastMessage().isEmpty() ? "§7(не задано)" : "§f" + config.broadcastMessage();
return ItemStackFactory.create(Material.BOOK, "§a§lСообщение при сбросе",
"§8───────────────",
"§7Текущее:",
" " + ItemStackFactory.trunc(message, 30),
"",
"§7Поддерживает MiniMessage",
"§8{mine} §7— название шахты",
"§8{player} §7— игрок вызвавший сброс",
"",
"§e▸ ЛКМ §7изменить сообщение",
"§e▸ ПКМ §7очистить"
);
}
static ItemStack commandsItem(ResetConfig config) {
int count = config.commands().size();
return ItemStackFactory.create(Material.COMMAND_BLOCK, "§a§lКоманды при сбросе",
"§8───────────────",
"§7Количество: §f" + count,
"",
"§7Плейсхолдеры:",
"§8{mine} §7— название шахты",
"§8{world} §7— мир",
"",
"§e▸ ЛКМ §7редактировать список"
);
}
static ItemStack backItem() {
return ItemStackFactory.create(Material.ARROW, "§c§lНазад",
"§8───────────────",
"§7Вернуться в редактор шахты",
"",
"§e▸ Нажмите для возврата"
);
}
}

View file

@ -1,9 +1,11 @@
package dev.loki.lomines.gui.mine.edit; 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.model.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.edit.rewards.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);
@ -71,49 +74,51 @@ public final class RewardsGui {
int itemsCount = entry.items().size(); int itemsCount = entry.items().size();
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 += "...";
} }
return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграда #" + index, return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграда #" + index,
"§8───────────────", "§8───────────────",
"§7Шанс: §f" + String.format("%.1f%%", entry.chance() * 100), "§7Шанс: §f" + String.format("%.1f%%", entry.chance() * 100),
"§7Блоки: §f" + blocksList, "§7Блоки: §f" + blocksList,
"§7Предметов: §f" + itemsCount, "§7Предметов: §f" + itemsCount,
"§7Команд: §f" + commandsCount, "§7Команд: §f" + commandsCount,
"§7Блокировать дроп: " + (entry.preventVanillaDrops() ? "§aда" : "§7нет"), "§7Блокировать дроп: " + (entry.preventVanillaDrops() ? "§aда" : "§7нет"),
"", "",
"§e▸ ЛКМ §7редактировать", "§e▸ ЛКМ §7редактировать",
"§e▸ ПКМ §7удалить" "§e▸ ПКМ §7удалить"
); );
} }
private static ItemStack addRewardItem() { private static ItemStack addRewardItem() {
return ItemStackFactory.create(Material.EMERALD_BLOCK, "§a§lДобавить награду", return ItemStackFactory.create(Material.EMERALD_BLOCK, "§a§lДобавить награду",
"§8───────────────", "§8───────────────",
"§7Создать новую запись", "§7Создать новую запись",
"§7награды", "§7награды",
"", "",
"§e▸ Нажмите для добавления" "§e▸ Нажмите для добавления"
); );
} }
private static ItemStack backItem() { private static ItemStack backItem() {
return ItemStackFactory.create(Material.ARROW, "§c§lНазад", return ItemStackFactory.create(Material.ARROW, "§c§lНазад",
"§8───────────────", "§8───────────────",
"§7Вернуться в редактор шахты", "§7Вернуться в редактор шахты",
"", "",
"§e▸ Нажмите для возврата" "§e▸ Нажмите для возврата"
); );
} }
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot,
String mineName, boolean leftClick, boolean rightClick, String mineName, boolean leftClick, boolean rightClick,
int rewardIndex) { int rewardIndex) {
if (rawSlot < 0 || rawSlot >= SIZE) return false; if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
if (rawSlot == SLOT_BACK) { if (rawSlot == SLOT_BACK) {
MineEditGui.open(plugin, player, mineName); MineEditGui.open(plugin, player, mineName);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,111 @@
package dev.loki.lomines.gui.mine.main;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
final class MineEditExtraItems {
private static final String SEP = "§8───────────────";
private static final String EDIT = "§e▸ Нажмите для редактирования";
private static final String SAVE = "§a▸ Нажмите для сохранения";
private static final String DEL = "§c▸ Нажмите для удаления";
private MineEditExtraItems() {
}
static ItemStack teleportItem(MineConfig config) {
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fТелепорт: " + (config.teleport().enabled() ? "§aвкл" : "§7выкл"));
if (config.teleport().enabled()) {
config.teleport().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc)));
}
lore.add("");
lore.add(" §fСпавн (застрявших): " + (config.playerSpawn().enabled() ? "§aвкл" : "§7выкл"));
if (config.playerSpawn().enabled()) {
config.playerSpawn().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc)));
}
lore.add("");
lore.add("§8Команды:");
lore.add(" §8• §7/lm setteleport §f<mine>");
lore.add(" §8• §7/lm setspawn §f<mine>");
lore.add(" §8• §7/lm clearspawn §f<mine>");
lore.add("");
lore.add(EDIT);
return ItemStackFactory.create(Material.ENDER_PEARL, "§a§lТелепорт", lore.toArray(new String[0]));
}
static ItemStack uiItem(MineConfig config) {
return ItemStackFactory.create(Material.PAINTING, "§a§lИнтерфейс",
SEP,
"§7Настройки:",
" §fAction Bar: " + (config.ui().actionBarEnabled() ? "§aвкл" : "§7выкл"),
" §fРадиус: §7" + (int) config.ui().actionBarRange() + " §8блоков",
"",
"§7Формат ActionBar:",
" §8" + ItemStackFactory.trunc(config.ui().actionBarFormat(), 28),
"",
"§7Формат таймера: §8" + config.ui().timerFormat(),
"",
EDIT
);
}
static ItemStack saveItem() {
return ItemStackFactory.create(Material.LIME_DYE, "§a§lСохранить изменения",
SEP,
"§7Сохраняет конфигурацию шахты",
"§7в файл на диске",
"",
SAVE
);
}
static ItemStack deleteItem(String mineName) {
return ItemStackFactory.create(Material.TNT, "§c§lУдалить шахту",
SEP,
"§c§lВнимание!",
"§cЭто действие нельзя отменить!",
"",
"§7Шахта: §f" + mineName,
"",
DEL
);
}
static ItemStack infoItem(Mine mine) {
int blocks = mine.getBlocks();
int total = mine.getTotalVolume();
double percent = mine.getPercentFilled();
String c = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c";
int filled = (int) Math.round(percent / 100.0 * 10);
String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(10 - filled);
return ItemStackFactory.create(Material.BOOK, "§b§lИнформация",
SEP,
"§7Базовые данные:",
" §fНазвание: §7" + mine.getName(),
" §fМир: §7" + mine.getConfig().worldName(),
"",
"§7Состояние заполнения:",
" §fБлоков: " + c + blocks + "§8/§7" + total,
" §fПроцент: " + c + String.format("%.1f%%", percent),
" " + bar
);
}
static ItemStack backItem() {
return ItemStackFactory.create(Material.ARROW, "§c§lЗакрыть",
SEP,
"§7Закрыть редактор",
"",
"§c▸ Нажмите чтобы закрыть"
);
}
}

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