chore: unify checkstyle, fix violations, cleanup bin/

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

25
.classpath Normal file
View file

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

34
.project Normal file
View file

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

View file

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

View file

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

View file

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

0
gradlew vendored Normal file → Executable file
View file

View file

@ -1,11 +1,11 @@
package dev.loki.lomines; 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,16 +1,13 @@
package dev.loki.lomines; package dev.loki.lomines;
import dev.loki.lomines.command.admin.*; import dev.loki.lomines.command.LmCommand;
import dev.loki.lomines.command.common.LoMinesTabCompleter;
import dev.loki.lomines.command.player.PlayerCommands;
import dev.loki.lomines.command.player.TeleportCommand;
import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.listener.*; import dev.loki.lomines.listener.block.BlockBreakListener;
import dev.lolib.commands.CommandManager; import dev.loki.lomines.listener.gui.GroupGuiListener;
import dev.loki.lomines.listener.gui.MineEditGuiListener;
import dev.loki.lomines.listener.player.PlayerInteractListener;
import dev.loki.lomines.listener.player.PlayerJoinListener;
/**
* Handles registration of commands, listeners, and integrations.
*/
final class RegistrationManager { final class RegistrationManager {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
@ -19,28 +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);
commandManager.register(new InfoCommand(plugin)); cmd.setTabCompleter(executor);
commandManager.register(new TeleportCommand(plugin));
commandManager.register(new CopyCommand(plugin));
commandManager.register(new RegionCommands(plugin));
plugin.loLogger().info("Commands registered");
} }
}
void registerTabCompleter() { plugin.getLogger().info("Commands registered");
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() {
@ -52,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);
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,12 @@
package dev.loki.lomines.command.admin; package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.model.MineConfig;
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.command.CommandSender; import org.bukkit.command.CommandSender;
/**
* Command to copy mine configuration from one mine to another.
*/
public class CopyCommand { public class CopyCommand {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
@ -20,15 +15,13 @@ public class CopyCommand {
this.plugin = plugin; this.plugin = plugin;
} }
/** public void handle(CommandSender sender, String[] args) {
* Copies mine configuration (blocks, reset settings, rewards, etc.) if (args.length < 2) {
* from source mine to target mine. Regions and positions are NOT copied. sender.sendMessage(Component.text("Usage: /lm copy <from> <to>", NamedTextColor.RED));
* Usage: /lm copy <from> <to> return;
*/ }
@Subcommand(value = "copy", permission = "lomines.admin.copy") String fromMineName = args[0];
void copy(CommandSender sender, String toMineName = args[1];
@Arg("from-mine") String fromMineName,
@Arg("to-mine") String toMineName) {
Mine fromMine = plugin.getMines().find(fromMineName).orElse(null); Mine fromMine = plugin.getMines().find(fromMineName).orElse(null);
if (fromMine == null) { if (fromMine == null) {
@ -50,7 +43,6 @@ public class CopyCommand {
var sourceConfig = fromMine.getConfig(); var sourceConfig = fromMine.getConfig();
var targetConfig = toMine.getConfig(); var targetConfig = toMine.getConfig();
// Keep target region/name, copy all other sections from source.
MineConfig newTargetConfig = MineConfig.builder(toMine.getName()) MineConfig newTargetConfig = MineConfig.builder(toMine.getName())
.region(targetConfig.region()) .region(targetConfig.region())
.blocks(sourceConfig.blocks()) .blocks(sourceConfig.blocks())

View file

@ -1,45 +1,31 @@
package dev.loki.lomines.command.admin; package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.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.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;
@ -72,12 +62,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;
@ -114,12 +99,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) {
@ -148,7 +128,7 @@ class TeleportCommands {
} }
} }
private String formatLocation(Location loc) { 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,17 +1,12 @@
package dev.loki.lomines.command.admin; package dev.loki.lomines.command.admin.stats;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.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.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.command.CommandSender; import org.bukkit.command.CommandSender;
/**
* Commands for managing holograms above mines.
*/
public class HologramCommands { public class HologramCommands {
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
@ -22,13 +17,14 @@ public class HologramCommands {
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));
@ -42,10 +38,9 @@ public 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 {

View file

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

View file

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

View file

@ -4,16 +4,10 @@ import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.command.Command; import org.bukkit.command.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 static final List<String> ALL_COMMANDS = List.of( private static final List<String> ALL_COMMANDS = List.of(
@ -21,12 +15,15 @@ public class LoMinesTabCompleter implements TabCompleter {
"stats", "top", "maskscan", "edit", "setteleport", "setspawn", "stats", "top", "maskscan", "edit", "setteleport", "setspawn",
"clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help" "clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help"
); );
private static final List<String> BOOLEAN_VALUES = List.of("true", "false");
private static final List<String> TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50");
private final LoMinesPlugin plugin; private final LoMinesPlugin plugin;
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
@ -39,169 +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", "info",
"regions", "addregion" -> filterStartsWith(getMineNames(), partial);
case "reset" -> completeReset(partial);
case "stats" -> completeStats(sender, partial);
case "top" -> completeTopFirstArg(partial);
case "tp" -> completeTeleport(sender, partial);
case "copy" -> completeCopyFirstArg(partial);
case "removeregion" -> completeRemoveRegion(sender, partial);
default -> new ArrayList<>();
};
} }
private List<String> completeThirdArg(String subcommand, String arg2, String partial) {
return switch (subcommand) {
case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial);
case "top" -> completeTopSecondArg(arg2, partial);
case "copy" -> filterStartsWith(getMineNames(), partial);
case "removeregion" -> completeRegionIndex(arg2, partial);
default -> new ArrayList<>();
};
}
private List<String> completeRegionIndex(String mineName, String partial) {
var mine = plugin.getMines().find(mineName);
if (mine.isEmpty()) {
return new ArrayList<>();
}
int regionCount = mine.get().getRegions().size();
List<String> indices = new ArrayList<>();
for (int i = 1; i <= regionCount; i++) {
indices.add(String.valueOf(i));
}
return filterStartsWith(indices, partial);
}
private List<String> completeCreate(String partial) {
if (partial.isEmpty()) {
return new ArrayList<>(List.of("<name>"));
}
return new ArrayList<>();
}
private List<String> completeReset(String partial) {
List<String> mines = filterStartsWith(getMineNames(), partial);
if (partial.isEmpty() || "true".startsWith(partial) || "false".startsWith(partial)) {
mines.addAll(filterStartsWith(BOOLEAN_VALUES, partial));
}
return mines;
}
private List<String> completeStats(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.stats.others")) {
return new ArrayList<>();
}
return filterStartsWith(getOnlinePlayerNames(), partial);
}
private List<String> completeTopFirstArg(String partial) {
List<String> completions = new ArrayList<>();
completions.addAll(filterStartsWith(getMineNames(), partial));
if (isNumericPartial(partial)) {
completions.addAll(filterStartsWith(TOP_LIMITS, partial));
}
return completions;
}
private List<String> completeTopSecondArg(String firstArg, String partial) {
boolean firstArgWasMine = plugin.getMines().find(firstArg).isPresent();
if (firstArgWasMine && isNumericPartial(partial)) {
return filterStartsWith(TOP_LIMITS, partial);
}
if (!firstArgWasMine && isNumericPartial(firstArg)) {
return filterStartsWith(getMineNames(), partial);
}
return new ArrayList<>();
}
private List<String> completeTeleport(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.teleport")) {
return new ArrayList<>();
}
return filterStartsWith(getMineNames(), partial);
}
private List<String> completeCopyFirstArg(String partial) {
return filterStartsWith(getMineNames(), partial);
}
private List<String> completeRemoveRegion(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.admin.regions")) {
return new ArrayList<>();
}
return filterStartsWith(getMineNames(), partial);
}
private boolean isNumericPartial(String partial) {
return partial.isEmpty() || partial.matches("\\d*");
}
private List<String> filterStartsWith(List<String> options, String partial) {
return options.stream()
.filter(s -> s.toLowerCase().startsWith(partial))
.collect(Collectors.toList());
}
private List<String> getMineNames() {
return plugin.getMines().getAll().stream()
.map(mine -> mine.getName())
.collect(Collectors.toList());
}
private List<String> getOnlinePlayerNames() {
return plugin.getServer().getOnlinePlayers().stream()
.map(Player::getName)
.collect(Collectors.toList());
}
private boolean hasPermission(CommandSender sender, String cmd) {
return switch (cmd) {
case "create", "delete", "reload", "list", "maskscan", "edit", "info" ->
sender.hasPermission("lomines.admin");
case "reset" -> sender.hasPermission("lomines.admin.reset");
case "setteleport" -> sender.hasPermission("lomines.admin.setteleport");
case "setspawn", "clearspawn" -> sender.hasPermission("lomines.admin.setspawn");
case "wand", "group" -> sender.hasPermission("lomines.admin.wand");
case "stats", "top" -> sender.hasPermission("lomines.stats");
case "tp" -> sender.hasPermission("lomines.teleport");
case "copy" -> sender.hasPermission("lomines.admin.copy");
case "regions", "addregion", "removeregion" -> sender.hasPermission("lomines.admin.regions");
case "help" -> sender.hasPermission("lomines.use");
default -> true;
};
}
}

View file

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

View file

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

View file

@ -3,19 +3,11 @@ package dev.loki.lomines.command.player;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.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,8 +18,9 @@ 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()) {
case "wand" -> {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
errorHandler.sendError(sender, "Только для игрока."); errorHandler.sendError(sender, "Только для игрока.");
return; return;
@ -37,15 +30,20 @@ public class PlayerCommands {
"Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>",
NamedTextColor.GREEN)); NamedTextColor.GREEN));
} }
case "group" -> {
@Subcommand(value = "group", permission = "lomines.admin.wand") if (args.length < 1) {
public void group(CommandSender sender, @Arg("prefix") String prefix) { sender.sendMessage(Component.text("Usage: /lm group <prefix>", NamedTextColor.RED));
return;
}
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
errorHandler.sendError(sender, "Префикс задаётся в игре."); errorHandler.sendError(sender, "Префикс задаётся в игре.");
return; return;
} }
plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix); plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(args[0]);
String p = prefix.trim(); String p = args[0].trim();
player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA)); player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA));
} }
default -> {}
}
}
} }

View file

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

View file

@ -1,32 +1,28 @@
package dev.loki.lomines.core.mine; package dev.loki.lomines.core.mine.model;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
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.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; 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;
@ -34,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;
@ -50,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() {
@ -100,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) {
@ -121,19 +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 -> case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("External plugins disabled");
throw new IllegalArgumentException("Oraxen integration is currently disabled"); case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("External plugins disabled");
case BlockKey.ItemsAdder itemsAdder ->
throw new IllegalArgumentException("ItemsAdder integration is currently disabled");
case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin); case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin);
}; };
} }
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() {
@ -152,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,22 +1,20 @@
package dev.loki.lomines.core.mine; package dev.loki.lomines.core.mine.registry;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.service.MaskScanService; import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.service.MineLoader;
import dev.loki.lomines.core.service.mask.MaskScanService;
import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.core.service.MineRepository; import dev.loki.lomines.core.service.MineRepository;
import dev.loki.lomines.data.config.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;
@ -25,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;
@ -33,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 {
@ -63,7 +63,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 {
@ -78,62 +78,56 @@ 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());
} }
} }
} }
/**
* Updates mine configuration and saves to disk.
* Used for modifying regions, blocks, etc.
*/
public void updateMineConfig(String name, MineConfig newConfig) throws IOException { public void updateMineConfig(String name, MineConfig newConfig) throws IOException {
if (!repository.exists(name)) { if (!repository.exists(name)) {
throw new IllegalArgumentException("Mine not found: " + name); throw new IllegalArgumentException("Mine not found: " + name);
} }
// Save new config to file
try { try {
fileManager.saveConfig(name, newConfig); fileManager.saveConfig(name, newConfig);
} catch (ConfigLoader.ConfigLoadException e) { } catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save mine config: " + e.getMessage(), e); throw new IOException("Failed to save mine config: " + e.getMessage(), e);
} }
// Reload to apply changes
try { try {
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());
} }

View file

@ -1,10 +1,10 @@
package dev.loki.lomines.core.mine; package dev.loki.lomines.core.mine.service;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.LoMinesPlugin;
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.ConfigLoader; import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.model.MineConfig;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -49,9 +49,9 @@ public 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);
} }

View file

@ -1,38 +1,25 @@
package dev.loki.lomines.core.service; package dev.loki.lomines.core.service;
import dev.loki.lomines.core.service.write.MineConfigWriter;
import dev.loki.lomines.data.config.ConfigLoader; import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig; 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.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,86 +32,9 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
} }
} }
/**
* Creates a default mine configuration with new format.
* WorldGuard region settings are loaded from defaults.yml.
*/
public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException { 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
try {
configLoader.save(config);
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save default mine config: " + e.getMessage(), e);
}
}
/**
* Loads WorldGuard configuration from defaults.yml.
*/
private WorldGuardConfig loadWorldGuardDefaults() {
Path defaultsPath = minesFolder.resolve("_defaults.yml");
if (!Files.exists(defaultsPath)) {
return WorldGuardConfig.disabled();
}
try {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard");
if (wgSection == null) {
return WorldGuardConfig.disabled();
}
boolean enabled = wgSection.getBoolean("enabled", false);
if (!enabled) {
return WorldGuardConfig.disabled();
}
String template = wgSection.getString("region-template", "{mine_name}_{random_4}");
return WorldGuardConfig.builder()
.enabled(true)
.template(template)
.owners(wgSection.getStringList("owners"))
.members(wgSection.getStringList("members"))
.flags(wgSection.getStringList("flags"))
.protectOnCreate(wgSection.getBoolean("protect-on-create", true))
.build();
} catch (Exception e) {
return WorldGuardConfig.disabled();
}
} }
public MineConfig loadConfig(String name) throws IOException, ConfigLoader.ConfigLoadException { public MineConfig loadConfig(String name) throws IOException, ConfigLoader.ConfigLoadException {
@ -138,64 +48,16 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
} }
} }
/**
* Saves a mine configuration to file.
*
* @param config the configuration to save
* @throws ConfigLoader.ConfigLoadException if saving fails
*/
public void saveConfig(MineConfig config) throws ConfigLoader.ConfigLoadException { public void saveConfig(MineConfig config) throws ConfigLoader.ConfigLoadException {
configLoader.save(config); configLoader.save(config);
} }
/**
* Saves a mine configuration to file by name.
*
* @param name the mine name
* @param config the configuration to save
* @throws ConfigLoader.ConfigLoadException if saving fails
*/
public void saveConfig(String name, MineConfig config) throws ConfigLoader.ConfigLoadException { public void saveConfig(String name, MineConfig config) throws ConfigLoader.ConfigLoadException {
// Ensure the config has the correct name
MineConfig configToSave = config;
if (!name.equals(config.region().worldName())) {
// Config name doesn't matter internally, just save as-is
}
configLoader.save(config); configLoader.save(config);
} }
/** public void saveMaskPositions(String name, BlockKey markerMaterial,
* Saves mask positions for mask fill mode. java.util.List<Location> positions) throws IOException, ConfigLoader.ConfigLoadException {
*/ configWriter.saveMaskPositions(name, markerMaterial, positions);
public void saveMaskPositions(String name, BlockKey markerMaterial, java.util.List<Location> positions) throws IOException, ConfigLoader.ConfigLoadException {
MineConfig config = loadConfig(name);
// Build new mask config
Map<String, Boolean> posMap = new HashMap<>();
for (Location loc : positions) {
posMap.put(LocationParser.format(loc), true);
}
BlockConfig.MaskConfig mask = new BlockConfig.MaskConfig(markerMaterial, posMap);
// Create new block config with mask
BlockConfig newBlocks = new BlockConfig(
config.blocks().weights(),
FillMode.MASK,
mask
);
// Build updated config
MineConfig updated = MineConfig.builder(name)
.region(config.region())
.blocks(newBlocks)
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
configLoader.save(updated);
} }
} }

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.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.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;

View file

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

View file

@ -8,24 +8,18 @@ import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader;
import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader; import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader;
import dev.loki.lomines.data.config.loader.system.ResetConfigLoader; 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,17 +92,6 @@ public final class ConfigLoader {
} }
} }
private void mergeDefaults(YamlConfiguration yaml) {
for (String key : defaults.getKeys(true)) {
if (!yaml.contains(key)) {
yaml.set(key, defaults.get(key));
}
}
}
/**
* Exception for config loading errors.
*/
public static class ConfigLoadException extends Exception { public static class ConfigLoadException extends Exception {
public ConfigLoadException(String message) { public ConfigLoadException(String message) {
super(message); super(message);

View file

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

View file

@ -32,7 +32,9 @@ public final class BlockConfigLoader {
Map<BlockKey, Double> weights = new HashMap<>(); 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);
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config; package dev.loki.lomines.data.config.model;
import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.region.RegionConfig; import dev.loki.lomines.data.config.region.RegionConfig;
@ -7,58 +7,13 @@ import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.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();
}
} }
/**
* 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,
@ -73,32 +28,6 @@ public record MineConfig(
); );
} }
/**
* Total volume of the mine (for progress calculation).
*/
public int volume() {
return region.totalVolume();
}
/**
* World name where the mine is located.
*/
public String worldName() {
return region.worldName();
}
/**
* Returns the spawn location for stuck players.
* If playerSpawn is not set, falls back to teleport location.
*/
public Optional<Location> getSpawnForStuckPlayer() {
return playerSpawn.enabled()
? playerSpawn.getLocation()
: teleport.getLocation();
}
// --- Builder ---
public static class Builder { 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

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

@ -3,9 +3,6 @@ package dev.loki.lomines.data.config.reset;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
/**
* Configuration for mine reset behavior.
*/
public record ResetConfig( public record ResetConfig(
Duration interval, Duration interval,
double percentTrigger, double percentTrigger,
@ -14,34 +11,16 @@ 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;
* Default config: 5 minutes, 10% trigger disabled. public static final double DEFAULT_PERCENT_TRIGGER = ResetConfigValidator.DEFAULT_PERCENT_TRIGGER;
*/
public static ResetConfig defaults() { public static ResetConfig defaults() {
return new ResetConfig( return new ResetConfig(
DEFAULT_INTERVAL, DEFAULT_INTERVAL,
@ -52,23 +31,14 @@ public record ResetConfig(
); );
} }
/**
* Builder for fluent construction.
*/
public static Builder builder() { public static Builder builder() {
return new Builder(); return new Builder();
} }
/**
* Interval in ticks (20 ticks = 1 second).
*/
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) {
@ -80,9 +50,6 @@ 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;
} }
@ -94,38 +61,18 @@ public record ResetConfig(
private List<String> commands = List.of(); private List<String> commands = List.of();
private String broadcastMessage = ""; private String broadcastMessage = "";
private static Duration parseDuration(String s) {
s = s.trim().toLowerCase();
try {
if (s.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("h")) {
return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("d")) {
return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1)));
} else {
// Assume seconds if no suffix
return Duration.ofSeconds(Long.parseLong(s));
}
} catch (NumberFormatException e) {
return DEFAULT_INTERVAL;
}
}
public Builder interval(Duration interval) { public Builder interval(Duration interval) {
this.interval = interval; this.interval = interval;
return this; return this;
} }
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;
} }

View file

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

View file

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

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

View file

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

View file

@ -34,12 +34,16 @@ public final class ItemStackFactory {
} }
public static String loc(Location l) { 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());
} }
public 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;
} }

View file

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

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.gui.group; 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;
@ -99,7 +99,9 @@ public final class GroupCreateGui {
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));
@ -109,7 +111,8 @@ 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();
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,13 @@
package dev.loki.lomines.gui.mine.edit.blocks; package dev.loki.lomines.gui.mine.edit.blocks.view;
import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.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 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;
@ -17,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;
@ -66,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++;
} }
@ -75,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);
@ -91,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.blocks; 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;

View file

@ -1,9 +1,9 @@
package dev.loki.lomines.gui.mine.edit.reset; 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;
@ -67,7 +67,9 @@ 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);

View file

@ -1,10 +1,10 @@
package dev.loki.lomines.gui.mine.edit.rewards; 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.reward.RewardConfig; import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.gui.common.ItemStackFactory; import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.holder.RewardsGuiHolder; import dev.loki.lomines.gui.mine.holder.edit.rewards.RewardsGuiHolder;
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;
@ -116,7 +116,9 @@ public final class RewardsGui {
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▸ Нажмите чтобы закрыть"
);
}
}

View file

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

View file

@ -1,7 +1,6 @@
package dev.loki.lomines.gui.mine.main; package dev.loki.lomines.gui.mine.main;
import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.reward.RewardConfig; import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.gui.common.ItemStackFactory; import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material; import org.bukkit.Material;
@ -17,25 +16,24 @@ final class MineEditItems {
private static final String SEP = "§8───────────────"; private static final String SEP = "§8───────────────";
private static final String EDIT = "§e▸ Нажмите для редактирования"; private static final String EDIT = "§e▸ Нажмите для редактирования";
private static final String CLOSE = "§c▸ Нажмите чтобы закрыть";
private static final String SAVE = "§a▸ Нажмите для сохранения";
private static final String DEL = "§c▸ Нажмите для удаления";
private MineEditItems() { private MineEditItems() {
} }
static ItemStack filler() { static ItemStack filler() {
return ItemStackFactory.filler(); return ItemStackFactory.filler();
} }
static ItemStack blocksItem(MineConfig config) { static ItemStack blocksItem(MineConfig config) {
var w = config.blocks().weights();
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add(SEP); lore.add(SEP);
lore.add("§7Настройки:"); lore.add("§7Настройки:");
lore.add(" §fТипов: §7" + config.blocks().blockCount()); lore.add(" §fТипов: §7" + config.blocks().blockCount());
lore.add(" §fРежим: §7" + config.blocks().fillMode().name()); lore.add(" §fРежим: §7" + config.blocks().fillMode().name());
lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет")); lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет"));
var w = config.blocks().weights();
if (!w.isEmpty()) { if (!w.isEmpty()) {
lore.add(""); lore.add("");
lore.add("§7Топ блоков:"); lore.add("§7Топ блоков:");
@ -78,10 +76,10 @@ final class MineEditItems {
} }
static ItemStack rewardsItem(MineConfig config) { static ItemStack rewardsItem(MineConfig config) {
var entries = config.rewards().entries(); final var entries = config.rewards().entries();
long items = entries.stream().flatMap(e -> e.items().stream()).count(); final long items = entries.stream().flatMap(e -> e.items().stream()).count();
long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count(); final long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count();
long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count(); final long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count();
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add(SEP); lore.add(SEP);
lore.add("§7Настройки:"); lore.add("§7Настройки:");
@ -91,100 +89,13 @@ final class MineEditItems {
lore.add("§7Типы наград:"); lore.add("§7Типы наград:");
lore.add(" §8• §fПредметов: §7" + items); lore.add(" §8• §fПредметов: §7" + items);
lore.add(" §8• §fКоманд: §7" + cmds); lore.add(" §8• §fКоманд: §7" + cmds);
if (blockDrop > 0) lore.add(" §8• §cБлокируют дроп: §7" + blockDrop); if (blockDrop > 0) {
lore.add(" §8• §cБлокируют дроп: §7" + blockDrop);
}
} }
lore.add(""); lore.add("");
lore.add(EDIT); lore.add(EDIT);
return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0])); return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0]));
} }
static ItemStack teleportItem(MineConfig config) {
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fТелепорт: " + (config.teleport().enabled() ? "§aвкл" : "§7выкл"));
if (config.teleport().enabled()) {
config.teleport().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc)));
}
lore.add("");
lore.add(" §fСпавн (застрявших): " + (config.playerSpawn().enabled() ? "§aвкл" : "§7выкл"));
if (config.playerSpawn().enabled()) {
config.playerSpawn().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc)));
}
lore.add("");
lore.add("§8Команды:");
lore.add(" §8• §7/lm setteleport §f<mine>");
lore.add(" §8• §7/lm setspawn §f<mine>");
lore.add(" §8• §7/lm clearspawn §f<mine>");
lore.add("");
lore.add(EDIT);
return ItemStackFactory.create(Material.ENDER_PEARL, "§a§lТелепорт", lore.toArray(new String[0]));
}
static ItemStack uiItem(MineConfig config) {
return ItemStackFactory.create(Material.PAINTING, "§a§lИнтерфейс",
SEP,
"§7Настройки:",
" §fAction Bar: " + (config.ui().actionBarEnabled() ? "§aвкл" : "§7выкл"),
" §fРадиус: §7" + (int) config.ui().actionBarRange() + " §8блоков",
"",
"§7Формат ActionBar:",
" §8" + ItemStackFactory.trunc(config.ui().actionBarFormat(), 28),
"",
"§7Формат таймера: §8" + config.ui().timerFormat(),
"",
EDIT
);
}
static ItemStack saveItem() {
return ItemStackFactory.create(Material.LIME_DYE, "§a§lСохранить изменения",
SEP,
"§7Сохраняет конфигурацию шахты",
"§7в файл на диске",
"",
SAVE
);
}
static ItemStack deleteItem(String mineName) {
return ItemStackFactory.create(Material.TNT, "§c§lУдалить шахту",
SEP,
"§c§lВнимание!",
"§cЭто действие нельзя отменить!",
"",
"§7Шахта: §f" + mineName,
"",
DEL
);
}
static ItemStack infoItem(Mine mine) {
int blocks = mine.getBlocks();
int total = mine.getTotalVolume();
double percent = mine.getPercentFilled();
String c = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c";
int filled = (int) Math.round(percent / 100.0 * 10);
String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(10 - filled);
return ItemStackFactory.create(Material.BOOK, "§b§lИнформация",
SEP,
"§7Базовые данные:",
" §fНазвание: §7" + mine.getName(),
" §fМир: §7" + mine.getConfig().worldName(),
"",
"§7Состояние заполнения:",
" §fБлоков: " + c + blocks + "§8/§7" + total,
" §fПроцент: " + c + String.format("%.1f%%", percent),
" " + bar
);
}
static ItemStack backItem() {
return ItemStackFactory.create(Material.ARROW, "§c§lЗакрыть",
SEP,
"§7Закрыть редактор",
"",
CLOSE
);
}
} }

View file

@ -1,209 +0,0 @@
package dev.loki.lomines.handler;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.util.block.BlockUpdateUtil;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.LocationParser;
import dev.lolib.scheduler.Scheduler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Handles mine reset operations.
* Ensures thread-safe reset execution and prevents concurrent resets.
*
* <p>Reset process:</p>
* <ol>
* <li>Check if reset is already running (atomic check)</li>
* <li>Fill all regions asynchronously via BlockSetter</li>
* <li>Wait for all regions to complete</li>
* <li>Execute post-reset actions in main thread</li>
* </ol>
*
* <p>Updated for new configuration system (v2).</p>
*/
public final class MineResetHandler {
private final Mine mine;
private final LoMinesPlugin plugin;
private final AtomicBoolean running = new AtomicBoolean(false);
public MineResetHandler(Mine mine, LoMinesPlugin plugin) {
this.mine = mine;
this.plugin = plugin;
}
/**
* Resets the mine by filling all regions with blocks.
* If a reset is already in progress, this call is ignored.
*
* @param silent If true, suppresses broadcast messages
*/
public void reset(boolean silent) {
if (!running.compareAndSet(false, true)) {
return;
}
try {
resetAllRegions(silent);
} catch (Exception e) {
running.set(false);
plugin.loLogger().error("Failed to reset mine " + mine.getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
/**
* Fills all regions asynchronously and coordinates completion.
*/
private void resetAllRegions(boolean silent) {
List<Cuboid> regions = mine.getRegions();
AtomicInteger completed = new AtomicInteger(0);
AtomicInteger totalBlocks = new AtomicInteger(0);
if (mine.getBlockSetter() == null) {
plugin.loLogger().warn("BlockSetter not initialized for mine " + mine.getName());
running.set(false);
return;
}
// Handle MASK fill mode
if (mine.getConfig().blocks().fillMode() == FillMode.MASK) {
List<Location> maskPositions = getMaskPositions();
if (maskPositions.isEmpty()) {
plugin.loLogger().warn("No mask positions found for mine " + mine.getName());
running.set(false);
return;
}
mine.getBlockSetter().fillAtLocations(maskPositions, placed ->
Scheduler.get(plugin).run(() -> onResetComplete(placed, silent)));
return;
}
// Handle CUBOID fill mode
for (Cuboid region : regions) {
mine.getBlockSetter().fill(region, blocksSet -> {
totalBlocks.addAndGet(blocksSet);
if (completed.incrementAndGet() == regions.size()) {
Scheduler.get(plugin).run(() -> {
onResetComplete(totalBlocks.get(), silent);
});
}
});
}
}
/**
* Gets mask positions from config.
*/
private List<Location> getMaskPositions() {
List<Location> positions = new ArrayList<>();
var mask = mine.getConfig().blocks().mask();
if (mask == null) {
return positions;
}
for (String posStr : mask.positions().keySet()) {
try {
Location loc = LocationParser.parse(posStr);
if (loc != null) {
positions.add(loc);
}
} catch (Exception e) {
plugin.loLogger().warn("Invalid mask position in mine " + mine.getName() + ": " + posStr);
}
}
return positions;
}
/**
* Called when all regions have been filled.
* Executes post-reset actions in the main thread.
*/
private void onResetComplete(int totalBlocks, boolean silent) {
mine.getBlocksAtomic().set(totalBlocks);
mine.getTicksAtomic().set(0);
executeResetCommands();
if (!silent) {
broadcastReset();
}
if (mine.getConfig().teleport().enabled()) {
teleportPlayers();
}
running.set(false);
}
/**
* Executes commands configured for reset.
*/
private void executeResetCommands() {
List<String> commands = mine.getConfig().reset().commands();
for (String command : commands) {
String parsed = command.replace("%mine%", mine.getName());
plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), parsed);
}
}
/**
* Broadcasts reset message to players.
*/
private void broadcastReset() {
String message = mine.getConfig().reset().broadcastMessage();
if (message != null && !message.isEmpty()) {
String formatted = message.replace("%mine%", mine.getName());
Component component = MiniMessage.miniMessage().deserialize(formatted);
plugin.getServer().broadcast(component);
}
}
/**
* Teleports ONLY stuck players (those inside solid blocks) to a safe location.
* Uses player-spawn location if configured, otherwise uses teleport location.
* Only teleports if player is suffocating in a block, not all players in mine!
*/
private void teleportPlayers() {
// Get spawn location for stuck players (player-spawn config) or fall back to teleport location
var destOpt = mine.getConfig().getSpawnForStuckPlayer();
if (destOpt.isEmpty()) {
return;
}
Location dest = destOpt.get();
if (dest.getWorld() == null) {
return;
}
// Find a safe teleport location (max 3 blocks up to avoid teleporting too high)
Location safeDest = BlockUpdateUtil.findSafeTeleportLocation(dest, 3);
for (Player p : dest.getWorld().getPlayers()) {
// Only teleport if player is INSIDE the mine AND is suffocating in a solid block
if (mine.contains(p.getLocation()) && isPlayerStuckInBlock(p)) {
p.teleport(safeDest);
}
}
}
/**
* Checks if player is stuck inside a solid block (suffocating).
*/
private boolean isPlayerStuckInBlock(Player player) {
Location loc = player.getLocation();
var block = loc.getBlock();
// Check if block at player position is solid (not air/liquid)
return block.getType().isSolid() && !block.isLiquid();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.integration.placeholder; package dev.loki.lomines.integration.placeholder;
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 me.clip.placeholderapi.expansion.PlaceholderExpansion; import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

View file

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

View file

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

View file

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

View file

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

View file

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

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