diff --git a/.classpath b/.classpath deleted file mode 100644 index 961b063..0000000 --- a/.classpath +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.project b/.project deleted file mode 100644 index 7624faf..0000000 --- a/.project +++ /dev/null @@ -1,34 +0,0 @@ - - - LoMines - Project LoMines created by Buildship. - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.buildship.core.gradleprojectnature - - - - 1783342209787 - - 30 - - org.eclipse.core.resources.regexFilterMatcher - node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ - - - - diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index f7f3856..0000000 --- a/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,13 +0,0 @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c62cb28..46d3c16 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,25 +1,24 @@ -# ARCHITECTURE.md — LoMines (Java, Paper) +# ARCHITECTURE.md — LoMines (Java Rewrite) -> **Лицензия:** GNU General Public License v3.0 -> Исходный код распространяется на условиях GPL-3. Любые производные работы обязаны публиковаться под той же лицензией. +> **Лицензия:** GNU General Public License v3.0 +> Исходный код распространяется на условиях GPL-3. Любые производные работы обязаны публиковаться под той же лицензией. > Полный текст: https://www.gnu.org/licenses/gpl-3.0.html ## Обзор -LoMines — Minecraft-плагин системы шахт для Paper 1.19.2+ (сборка на API 1.21.4, Java 21). -Плагин написан на чистом Java без сторонних minecraft-фреймворков и реализует: +LoMines — Minecraft-плагин системы шахт, основанный на Paper API. +Данный документ описывает архитектуру **Java-версии** (переписана с Kotlin). +В качестве базовой библиотеки используется **LoAPI (lolib 2.0.1)**. -- Управление шахтами (создание, удаление, сброс, перезагрузка, конфигурация) -- Мультирегиональные шахты (до 5 пар точек → 5 `Cuboid`-регионов) -- Два режима заполнения: `CUBOID` (весь регион) и `MASK` (только по меткам) -- Взвешенные блоки (шансы нормализуются автоматически) -- Авто-сбросы: по интервалу (таймер) или по проценту добытых блоков -- WorldGuard-интеграцию (авто-создание/обновление/удаление регионов) -- GUI-редактор шахт (main/confirm/group) -- Статистику игроков и лидерборды (кэшируемые) -- PlaceholderAPI-интеграцию -- Голограммы (DecentHolograms / HolographicDisplays — провайдеры) -- Групповую палку для массового создания шахт, wand-частицы +Плагин реализует: +- Управление шахтами (создание, удаление, сброс, конфигурация) +- Мультирегиональные шахты (до 10 `Cuboid`-регионов, по 5 пар точек) +- Быструю/стандартную установку блоков с поддержкой Oraxen и ItemsAdder +- Систему наград при добыче блоков +- Статистику игроков и лидерборды +- Интеграции с PlaceholderAPI и WorldGuard +- GUI-редактор шахт +- Визуализацию выделения частицами --- @@ -27,272 +26,538 @@ LoMines — Minecraft-плагин системы шахт для Paper 1.19.2+ | Зависимость | Scope | Откуда / Зачем | |---|---|---| -| `paper-api:1.21.4-R0.1-SNAPSHOT` | compileOnly | Paper API | -| `commons-math3:3.6.1` | implementation | `EnumeratedDistribution` — взвешенный рандом | -| `commons-io:2.18.0` | implementation | утилиты для работы с файлами | +| `paper-api:1.21-R0.1-SNAPSHOT` | compileOnly | Paper API | +| `lolib-2.0.1.jar` | implementation (shadow) | LoAPI — базовая библиотека (scheduler, commands, gui, items, utils, config, metrics, placeholders, performance) | | `placeholderapi:2.11.6` | compileOnly | PlaceholderAPI | -| `worldguard-bukkit:7.0.13` | compileOnly | WorldGuard | -| `worldedit-bukkit:7.3.10` | compileOnly | WorldEdit (нужен для WG API) | -| `junit-jupiter:5.11.4` | test | JUnit 5 | -| `kotest-*:5.9.1` | test | Property-based тесты и ассерты Kotest | -| `mockito-core / mockito-inline` | test | Моки для тестов | -| `adventure-api / minimessage` | test | MiniMessage в тестах | +| `worldguard-bukkit:7.0.9` | compileOnly | WorldGuard | +| `oraxen:1.161.0` | compileOnly | Oraxen custom blocks | +| `api-itemsadder:3.6.1` | compileOnly | ItemsAdder custom blocks | +| `commons-math3:3.6.1` | compileOnly | `EnumeratedDistribution` — взвешенный рандом для блоков и наград | +| `commons-io:2.17.0` | compileOnly | `FileUtils.listFiles()` для сканирования папки mines/ | +| `junit-jupiter:5.11.3` | testImplementation | JUnit тесты | +| `kotest-runner-junit5:5.9.1` | testImplementation | Property-based тесты | -> Сборка: Gradle + shadow (`build/libs/LoMines-3.0.0.jar`). Проверка качества: Checkstyle (`config/checkstyle/checkstyle.xml`), severity=error. +> LoAPI шейдится в JAR плагина. Relocate: `dev.lolib` → `com.loki.lomines.libs.lolib` --- ## Структура пакетов ``` -dev.loki.lomines/ +com.loki.lomines/ │ -├── LoMinesPlugin.java # главный класс, extends JavaPlugin -├── ComponentInitializer.java # создание компонентов, загрузка шахт, тикер, статистика -├── RegistrationManager.java # регистрация команд, слушателей, интеграций +├── LoMinesPlugin.java # extends LoPlugin (LoAPI) │ -├── command/ -│ ├── LmCommand.java # корневой executor (/lm + alias) -│ ├── common/ # LoMinesTabCompleter, SubcommandCompleter, PermissionPredicate -│ ├── player/ # PlayerCommands (wand/group/help), TeleportCommand (tp) -│ └── admin/ -│ ├── manage/ # AdminCommands (create/delete/edit/reset/reload/list), CopyCommand, MineActionHandler -│ ├── info/ # InfoCommand, RegionCommands, RegionActionHandler -│ ├── misc/ # MaskCommands (maskscan), TeleportCommands (setteleport/setspawn/clearspawn), TeleportActionHandler -│ └── stats/ # StatsCommands (stats/top), HologramCommands (hologram), LeaderboardRenderer -│ -├── core/ -│ ├── mine/ -│ │ ├── model/ # Mine, MineState -│ │ ├── registry/ # Mines, MineFinder -│ │ └── service/ # MineLoader, MineTicker -│ └── service/ -│ ├── MineFileManager.java # файловый доступ к mines/ -│ ├── MineRepository.java # in-memory реестр Mine -│ ├── write/ # MineConfigWriter -│ └── mask/ # MaskScanService -│ -├── data/ -│ ├── config/ -│ │ ├── ConfigLoader.java # парсинг/сохранение mine.yml через подлоадеры -│ │ ├── DefaultsMerger.java # наследование значений из _defaults.yml -│ │ ├── model/ # MineConfig (record), MineConfigDefaults -│ │ ├── block/ # BlockConfig, BlockKey, FillMode -│ │ ├── region/ # RegionConfig -│ │ ├── reset/ # ResetConfig, ResetConfigValidator -│ │ ├── reward/ # RewardConfig -│ │ ├── teleport/ # TeleportConfig -│ │ ├── spawn/ # PlayerSpawnConfig -│ │ ├── ui/ # UIConfig, HologramConfig -│ │ ├── parser/ # ConfigParseException -│ │ └── loader/ # подлоадеры по секциям (block, entity, region, reward, system) -│ ├── reward/ -│ │ ├── entity/ # Reward -│ │ └── parse/ # RewardParser, RewardEntryParser, command/, item/ +├── commands/ +│ ├── LoMinesCommand.java # @Command("lm") — корневая команда /lm +│ ├── admin/ +│ │ ├── MineManagementCommands.java # create, delete, redefine +│ │ ├── MineControlCommands.java # reset, reload, list +│ │ └── DebugCommands.java # testtime, checktime +│ ├── player/ +│ │ ├── MineNavigationCommands.java # wand, tp, settp +│ │ ├── MineEditorCommands.java # editor +│ │ └── NotificationCommands.java # messages, notify +│ ├── region/ +│ │ └── WorldGuardCommands.java # region create/delete/process │ └── stats/ -│ ├── model/ # PlayerStats, Leaderboard, LeaderboardEntry -│ └── service/ # StatsManager, StatsPersistence +│ └── StatsCommands.java # stats, top, stats reset │ -├── block/ -│ ├── BlockSetter.java # абстрактный setter (fill / fillAtLocations) -│ ├── BukkitBlockSetter.java # vanilla через Bukkit API (async fill) -│ ├── OraxenBlockSetter.java.disabled # отключено (внешние плагины выпилены) -│ └── ItemsAdderBlockSetter.java.disabled +├── config/ +│ └── impl/ +│ ├── Config.java # config.yml — глобальные настройки +│ ├── Messages.java # messages.yml — локализация +│ └── MineConfig.java # mines/.yml — конфиг шахты │ -├── handler/ -│ ├── reset/ # MineResetHandler, PlayerTeleportHandler -│ ├── block/ # MineBlockHandler -│ ├── reward/ # MineRewardHandler -│ └── ui/ # ActionBarHandler +├── mines/ +│ ├── Mine.java # Основной класс шахты +│ ├── Mines.java # Реестр всех шахт (static singleton) +│ ├── MineTicker.java # Тик всех шахт — Scheduler из LoAPI +│ │ +│ ├── handlers/ +│ │ ├── MineBlockHandler.java # Обработка BlockBreakEvent +│ │ ├── MineResetHandler.java # Логика сброса (fill + уведомления) +│ │ ├── MineRewardHandler.java # Выдача наград, парсинг конфига +│ │ ├── MineActionBarHandler.java # ActionBar — ActionBar из LoAPI utils +│ │ └── MineConfigLoader.java # Загрузка кубоидов, создание BlockSetter +│ │ +│ ├── setter/ +│ │ ├── BlockSetter.java # Абстрактный класс +│ │ ├── BukkitBlockSetter.java # Стандартный Bukkit (setBlockData) +│ │ ├── OraxenBlockSetter.java # Oraxen custom blocks +│ │ └── ItemsAdderBlockSetter.java # ItemsAdder custom blocks +│ │ +│ └── editor/ +│ ├── base/ +│ │ ├── BaseEditor.java # Базовый GUI — InventoryGUI из LoAPI +│ │ ├── BasePaginatedEditor.java # Пагинация — PagedGUI из LoAPI +│ │ └── EditorUtils.java +│ ├── MinesEditor.java +│ ├── MineEditor.java +│ ├── MineContentsEditor.java +│ ├── MineRewardsEditor.java +│ ├── MineRewardEditor.java +│ └── reward/ +│ ├── RewardCommandsEditor.java +│ ├── RewardItemsEditor.java +│ └── RewardBlocksEditor.java +│ +├── stats/ +│ ├── PlayerStats.java # Данные одного игрока (AtomicLong, ConcurrentHashMap) +│ ├── StatsManager.java # Центральный менеджер статистики +│ ├── StatsStorage.java # Чтение/запись stats.yml +│ └── Leaderboard.java # Топы по total и по шахтам +│ +├── selection/ +│ ├── Selection.java # До 5 пар точек (pos1..pos10), currentPair +│ ├── SelectionWand.java # Инструмент выделения + частицы +│ └── SelectionPairGui.java # GUI выбора пары — InventoryGUI из LoAPI +│ +├── integrations/ +│ ├── PlaceholderAPIIntegration.java # extends PlaceholderExpansion +│ ├── WorldGuardIntegration.java +│ └── WGRegionManager.java │ ├── listener/ -│ ├── block/ # BlockBreakListener -│ ├── gui/ # MineEditGuiListener, GuiActionHandler, GroupGuiListener -│ └── player/ # PlayerInteractListener, PlayerJoinListener +│ └── BlockListener.java # BlockBreakEvent, PlayerInteractEvent, PlayerJoinEvent │ -├── gui/ -│ ├── common/ # ItemStackFactory -│ ├── confirm/ # ConfirmDeleteGui, ConfirmDeleteGuiHolder -│ ├── group/ # GroupCreateGui, GroupCreateGuiHolder, GroupCreateItems -│ └── mine/ -│ ├── main/ # MineEditGui, MineEditItems, MineEditExtraItems -│ ├── edit/ -│ │ ├── blocks/ # view/ (BlocksGui, BlocksGuiItems), edit/ (BlockWeightEditor), select/ (BlockMaterialSelectionGui, BlockMaterialSelector) -│ │ ├── reset/ # ResetGui, ResetGuiItems -│ │ └── rewards/ # RewardsGui -│ └── holder/ # holders GUI (main/, edit/) +├── converters/ +│ └── CataMinesConverter.java # Конвертация из CataMines │ -├── integration/ -│ ├── IntegrationManager.java # init всех интеграций -│ ├── placeholder/ # LoMinesPlaceholderExpansion -│ ├── hologram/ # HologramManager, HologramProvider, HologramRenderer, provider/ (DecentHolograms, HolographicDisplays) -│ └── worldguard/ -│ ├── config/ # WorldGuardConfig, RegionTemplateConfig -│ ├── flag/ # WorldGuardFlagParser -│ └── region/ # WorldGuardRegionService, RegionTemplateRenderer, WorldGuardMemberHandler -│ -├── wand/ -│ ├── WandParticleService.java, ParticleUtil.java -│ └── group/ # GroupWandItem, GroupWandManager, GroupWandSession -│ -└── util/ - ├── ValidationUtils.java, ErrorHandler.java, MessageFormatter.java - ├── block/ # BlockUpdateUtil, SafeTeleportFinder - ├── format/ # TimeFormatter, ChunkUtils, ChunkRefresher, color/ (ColorUtils, HexColorConverter, LegacyColorConverter) - ├── location/ # BlockKeys, LocationParser, geo/ (Cuboid), safe/ (SafeTeleportUtil) - └── selection/ # Selection, SelectionManager, MaskScanner +└── utils/ + ├── ChunkUtils.java # sendMultiBlockChange / sendBlockChange по чанкам + ├── FileUtils.java # Пути к папке плагина, копирование ресурсов + ├── NotificationManager.java # Отключение уведомлений — ConcurrentHashSet + yaml + └── TimeUtils.java # Форматирование времени — TimeFormatter из LoAPI ``` --- -## Жизненный цикл плагина +## Использование LoAPI -``` -LoMinesPlugin.onEnable() - ├── saveDefaultConfig() # config.yml из resources - ├── ComponentInitializer.createDirectories() # dataFolder/mines/ - ├── ComponentInitializer.initialize() # Mines, GroupWandManager, StatsManager, IntegrationManager - ├── loadMines(mines) # Mines.loadAll() → MineLoader → MineRepository - ├── startTicker(mines) # MineTicker (runTaskTimer 1L, 1L) - ├── RegistrationManager.registerCommands() # /lm + aliases (lomines, mine, mines) - ├── RegistrationManager.registerListeners() # BlockBreak, PlayerInteract, GroupGui, MineEditGui, PlayerJoin - ├── initializeIntegrations() # PAPI + WorldGuard detection - └── startStatistics(statsManager) # load() + autosave (если statistics-enabled) +### Главный класс — `LoPlugin` -LoMinesPlugin.onDisable() - ├── mineTicker.stop() - ├── mines.getAll().forEach(Mine::stop) # отмена actionbar task - ├── statsManager.stopAutoSave() + save() # stats.yml - ├── integrationManager.shutdown() # unregister PAPI - ├── wandParticleService.stopAll() - └── hologramManager.shutdown() +`LoMinesPlugin` наследует `LoPlugin` вместо `JavaPlugin`. + +```java +public class LoMinesPlugin extends LoPlugin { + + public static LoMinesPlugin INSTANCE; + + @Override + protected void enable() { + INSTANCE = this; + + loLogger().info("LoMines включается..."); + + // Определение активных интеграций + SimpleFeatureFlags features = SimpleFeatureFlags.create() + .enable("placeholderapi", Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) + .enable("worldguard", Bukkit.getPluginManager().getPlugin("WorldGuard") != null) + .enable("oraxen", Bukkit.getPluginManager().getPlugin("Oraxen") != null) + .enable("itemsadder", Bukkit.getPluginManager().getPlugin("ItemsAdder") != null) + .enable("statistics", Config.STATISTICS_ENABLED); + + // Команды через CommandManager из LoAPI + CommandManager commands = new CommandManager(this); + commands.registerTabCompleter("mine", ctx -> new ArrayList<>(Mines.getTypes().keySet())); + commands.register(new LoMinesCommand()); + commands.register(new MineManagementCommands()); + commands.register(new MineControlCommands()); + commands.register(new DebugCommands()); + commands.register(new MineNavigationCommands()); + commands.register(new MineEditorCommands()); + commands.register(new NotificationCommands()); + commands.register(new WorldGuardCommands()); + commands.register(new StatsCommands()); + + Bukkit.getPluginManager().registerEvents(new BlockListener(), this); + + features.ifEnabled("placeholderapi", () -> new PlaceholderAPIIntegration().register()); + features.ifEnabled("worldguard", this::initWorldGuard); + + // Метрики через LoAPI Metrics + features.ifEnabled("statistics", () -> { + Metrics metrics = new Metrics(this, 20058); + metrics.addCustomChart(new Metrics.SimplePie("mines_count", + () -> String.valueOf(Mines.getTypes().size()))); + }); + + MineTicker.schedule(this); + SelectionWand.startParticleTask(this); + NotificationManager.init(); + StatsManager.load(); + scheduleStatsSave(); + reload(); + + loLogger().info("LoMines v" + getDescription().getVersion() + " включен!"); + } + + @Override + protected void disable() { + SelectionWand.stopParticleTask(); + StatsManager.save(); + Scheduler.get(this).cancelAll(); + loLogger().info("LoMines выключен!"); + } + + @Override + protected void dependencies(DependencyManager manager) { + // Runtime-загрузка зависимостей которых нет в LoAPI + manager.add("org.apache.commons", "commons-math3", "3.6.1"); + manager.add("commons-io", "commons-io", "2.17.0"); + } + + public void reload() { + Config.reload(); + Mines.reload(); + } +} ``` -Все компоненты создаются в `ComponentInitializer` и прокидываются через конструкторы — инъекция зависимостей вручную, без DI-фреймворка. +--- + +### Scheduler + +`MineTicker` и все задержанные/асинхронные операции используют `Scheduler` из LoAPI. +Работает на Paper и Folia без изменений кода. + +```java +// MineTicker.java — тик всех шахт +public class MineTicker { + public static void schedule(Plugin plugin) { + Scheduler.get(plugin).runTimer(() -> + Mines.getTypes().forEach((name, mine) -> mine.tick()), 1L, 1L); + } +} + +// Частицы выделения каждые 10 тиков +Scheduler.get(plugin).runTimer(SelectionWand::tickParticles, 0L, 10L); + +// Автосохранение статистики (async, не блокирует main thread) +Scheduler.get(this).runTimerAsync(() -> StatsManager.save(), intervalTicks, intervalTicks); + +// Sync-коллбэк после завершения fill (вызывается из callback BlockSetter) +Scheduler.get(plugin).run(() -> { + executeResetCommands(); + sendResetNotifications(silent); + teleportPlayers(); + running.set(false); +}); + +// Обновление чанков с задержкой 2 тика +Scheduler.get(plugin).runLater(() -> ChunkUtils.refreshChunksForPlayers(cuboid), 2L); +``` + +--- + +### Команды — `CommandManager` + +Все команды используют аннотации LoAPI: `@Command`, `@Subcommand`, `@Arg`, `@Cooldown`, `@PlayerOnly`, `@Permission`. + +```java +@Command(value = "lm", aliases = {"lomines"}) +public class LoMinesCommand { + + @Default + public void onHelp(CommandSender sender) { /* help */ } + + @Subcommand(value = "reload", permission = "lomines.command.reload") + public void onReload(CommandSender sender) { + LoMinesPlugin.INSTANCE.reload(); + sender.sendMessage(Messages.RELOAD); + } + + @Subcommand(value = "list", permission = "lomines.command.list") + public void onList(CommandSender sender) { + Mines.getTypes().forEach((name, mine) -> sender.sendMessage("- " + name)); + } +} + +@Command(value = "lm") +public class MineControlCommands { + + @Subcommand(value = "reset", permission = "lomines.command.reset") + public void onReset(CommandSender sender, + @Arg(value = "mine", completer = "mine") String mineName) { + Mine mine = Mines.valueOf(mineName); + mine.reset(false); + } +} + +@Command(value = "lm") +public class MineNavigationCommands { + + @Subcommand(value = "tp", permission = "lomines.command.teleport") + @PlayerOnly + @Cooldown(seconds = 3) + public void onTp(Player player, + @Arg(value = "mine", completer = "mine") String mineName) { + Mine mine = Mines.valueOf(mineName); + player.teleport(Serializers.LOCATION.deserialize(mine.config.TELEPORT_LOCATION)); + } + + @Subcommand(value = "wand", permission = "lomines.command.wand") + @PlayerOnly + public void onWand(Player player) { + player.getInventory().addItem(SelectionWand.getWand()); + } +} + +@Command(value = "lm") +public class StatsCommands { + + @Subcommand(value = "stats", permission = "lomines.command.stats") + @PlayerOnly + public void onStats(Player player) { + PlayerStats stats = StatsManager.getStats(player); + player.sendMessage("Всего сломано: " + stats.getTotalBlocks()); + } + + @Subcommand(value = "top", permission = "lomines.command.top") + public void onTop(CommandSender sender) { + Leaderboard.getTopTotal(10).forEach(entry -> + sender.sendMessage(entry.position() + ". " + entry.playerName() + " — " + entry.blocks())); + } +} +``` + +--- + +### GUI — `InventoryGUI` + +Редакторы шахт используют `InventoryGUI` из LoAPI. + +```java +// BaseEditor.java +public abstract class BaseEditor { + protected final Player player; + protected final InventoryGUI gui; // dev.lolib.gui.InventoryGUI + + public BaseEditor(Player player, String title, int rows) { + this.player = player; + this.gui = new InventoryGUI(LoMinesPlugin.INSTANCE, Component.text(title), rows); + } + + protected void setItem(int slot, ItemStack item, Consumer onClick) { + gui.setItem(slot, item, onClick::accept); + } + + public void open() { + buildItems(); + gui.open(player); + } + + protected abstract void buildItems(); +} + +// MineContentsEditor.java +public class MineContentsEditor extends BaseEditor { + public MineContentsEditor(Player player, Mine mine) { + super(player, "Блоки: " + mine.name, 6); + } + + @Override + protected void buildItems() { + setItem(49, ItemBuilder.of(Material.LIME_DYE).name("Добавить").build(), + p -> openAddBlockMenu(p)); + // ... слоты с блоками + } +} +``` + +--- + +### ItemBuilder + +```java +import dev.lolib.items.ItemBuilder; + +// Кнопка сброса +ItemStack btn = ItemBuilder.of(Material.REDSTONE) + .name("Сбросить шахту") + .lore("Заполнение: " + percent + "%") + .build(); + +// Заглушка +ItemStack filler = ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build(); + +// Палочка выделения +ItemStack wand = ItemBuilder.of(Material.GOLDEN_AXE) + .name("<#00AAFF>Палочка выделения") + .lore("<#00AAFF>ЛКМ — позиция #1") + .lore("<#00AAFF>ПКМ — позиция #2") + .lore("<#00AAFF>Shift+ПКМ — выбор региона") + .persistentData(wandKey, PersistentDataType.BYTE, (byte) 0) + .build(); +``` + +--- + +### Логирование — `LoLogger` + +```java +// Вместо getLogger() везде используем loLogger() +private static final LoLogger LOGGER = LoMinesPlugin.INSTANCE.loLogger(); + +LOGGER.info("Шахта " + name + " создана, объём: " + volume); +LOGGER.warn("Мир не найден для шахты " + name); +LOGGER.error("Не удалось создать BlockSetter", e); +if (Config.DEBUG) LOGGER.debug("Сброс занял " + took + "мс"); +``` + +--- + +### Форматирование — `StringUtils` / `TimeFormatter` + +```java +import dev.lolib.utils.StringUtils; +import dev.lolib.utils.TimeFormatter; + +// MiniMessage строки с плейсхолдерами +String msg = StringUtils.format(mine.config.ACTION_BAR, + "notbroken", String.valueOf(blocks), + "total", String.valueOf(mine.volume), + "percent", String.format("%.2f", blocks / mine.volume * 100.0), + "blocksbroken", String.format("%.2f", mine.volume - blocks), + "time", formatTime(remainingMs, mine.config.TIMER_FORMAT) +); + +// Форматирование времени по TIMER_FORMAT +private String formatTime(long ms, int format) { + Duration d = Duration.ofMillis(ms); + return switch (format) { + case 1 -> TimeFormatter.formatDigital(d); // "01:25:35" + case 2 -> TimeFormatter.formatShort(d); // "1ч 25м" + default -> TimeFormatter.formatSmart(d); // "01ч 25м 35с" + }; +} +``` --- ## Ключевые классы -### `Mine` (`core/mine/model/Mine.java`) +### `Mine.java` -Координатор одной шахты. Бизнес-логика делегирована в handler-ы: +Координирует handler-ы. Никакой логики — только делегирование. -| Метод | Делегирует | -|---|---| -| `reset(silent)` | `MineResetHandler.reset` | -| `onBlockBreak(player, block)` | `MineBlockHandler.handle` | -| `contains(location)` / `appliesToBlock(location)` | `RegionConfig` / `MineState` | -| `start()` / `stop()` | actionbar task | +```java +public class Mine { + public final String name; + public final MineConfig config; + public final List cuboids; // до 5 кубоидов -Поля: `name`, `regions` (List), `config` (MineConfig), `blockSetter`, `state` (MineState), `resetHandler`, `blockHandler`, `actionBarHandler`, `maskBlockKeys`. + private final MineBlockHandler blockHandler; + private final MineResetHandler resetHandler; + private final MineRewardHandler rewardHandler; + private final MineActionBarHandler actionBarHandler; + private final MineConfigLoader configLoader; -### `MineState` (`core/mine/model/MineState.java`) + private BlockSetter placer; + public double volume; // суммарный объём всех кубоидов + public double blocks; // текущее кол-во неломаных блоков + private long tick; // тик с последнего сброса -Потокобезопасное состояние шахты: -- `AtomicInteger blocks`, `AtomicInteger ticks` -- `totalVolume` — для MASK = число позиций маски, для CUBOID = объём регионов -- `getPercentFilled()` = `blocks / totalVolume * 100` - -### `Mines` (`core/mine/registry/Mines.java`) - -Фасад над реестром и сервисами: `MineFileManager` (файлы), `MineRepository` (in-memory), `MineLoader` (загрузка), `MaskScanService`, `WorldGuardRegionService`, `MineFinder` (поиск). Методы: `loadAll`, `create`, `delete`, `get/find/getAll/findByLocation`, `reloadMine`, `updateMineConfig`, `scanAndSaveMask`. - -### `MineRepository` (`core/service/MineRepository.java`) - -`ConcurrentHashMap` (ключ — имя в lowercase). `createAndStart(name, config)` → `new Mine` + `add` + `start`. `reload` = stop → loadConfig → createAndStart. - -### `MineFileManager` (`core/service/MineFileManager.java`) - -Файловые операции в `plugins/LoMines/mines/`: `ensureFolderExists`, `createDefaultConfig`, `loadConfig`, `saveConfig`, `deleteConfig`, `saveMaskPositions`. Оборачивает `ConfigLoader` + `MineConfigWriter`. - -### `MineTicker` (`core/mine/service/MineTicker.java`) - -Один общий `BukkitTask` (1L, 1L) тикает все шахты. Если `ticks >= reset.intervalTicks()` → сброс и обнуление счётчика. - -### `MineResetHandler` (`handler/reset/MineResetHandler.java`) - -Потокобезопасный сброс: - -``` -reset(silent) - ├── running.compareAndSet(false, true) # защита от двойного сброса - ├── MASK → blockSetter.fillAtLocations(maskPositions, cb) - ├── CUBOID → для каждого Cuboid: blockSetter.fill(region, cb) - │ когда все регионы завершены (AtomicInteger completed): - │ Bukkit.runTask(plugin) → onResetComplete(placed, silent) - └── onResetComplete: - blocks.set(placed); ticks.set(0) - executeResetCommands() # %mine% → имя шахты - broadcastReset() # MiniMessage - teleportStuckPlayers() # если teleport.enabled - running.set(false) + public void tick() { ... } + public void onBlockBreak(Player player, Block block, BlockBreakEvent event) { ... } + public void reset(boolean silent) { ... } + public void reload(boolean reset) { ... } + public boolean containsLocation(Location loc) { ... } // any cuboid + public double getDistanceToMine(Location loc) { ... } // min dist to any cuboid + public World getWorld() { ... } // world of first cuboid +} ``` -### `MineBlockHandler` (`handler/block/MineBlockHandler.java`) +### `Mines.java` -`handle(player, block)`: -1. `blocks.decrementAndGet()` -2. `rewardHandler.checkRewards(player, block)` -3. статистика: `statsManager.incrementBlocks(uuid, mineName)` -4. если `percentEnabled && percent <= percentTrigger` → `mine.reset(false)` +```java +public final class Mines { + private static final Map TYPES = new HashMap<>(); + // ключ = mine.name.toLowerCase() -### `BlockSetter` (`block/BlockSetter.java`) - -Абстракция стратегии заполнения: `fill(Cuboid, IntConsumer)` и `fillAtLocations(List, IntConsumer)`. Единственная активная реализация — `BukkitBlockSetter` (заполнение в async-задаче, затем `BlockUpdateUtil` отправляет пакеты обновления в main thread, чтобы не было ghost blocks). Oraxen/ItemsAdder-реализации отключены (`.disabled`) — внешние плагины выпилены в пользу vanilla. - -> В `Mine.createBlockSetter` выбор по типу первого `BlockKey`: `Vanilla → BukkitBlockSetter`, `Oraxen/ItemsAdder → throw IllegalArgumentException("External plugins disabled")`. - -### `MineRewardHandler` (`handler/reward/MineRewardHandler.java`) - -По сломанному блоку ищет награды в `RewardConfig.forBlock(blockKey)`; если `entry.roll(random)` → выдать items в инвентарь + выполнить команды консолью (плейсхолдеры `%player%`, `%uuid%`). - -### `ActionBarHandler` (`handler/ui/ActionBarHandler.java`) - -Раз в 10 тиков шлёт actionbar игрокам в радиусе от центра первого региона. Формат — MiniMessage из `ui.actionbar.format`. - -### `PlayerTeleportHandler` (`handler/reset/PlayerTeleportHandler.java`) - -После сброса телепортирует **только застрявших в блоке** игроков (solid + не liquid) внутри шахты в безопасную точку (`player-spawn` → fallback `teleport.location`), через `SafeTeleportFinder`. - ---- - -## Конфигурация - -Файлы в `plugins/LoMines/`: - -| Файл | Назначение | -|---|---| -| `config.yml` | Глобальные настройки: `statistics-enabled`, `debug`, `reset-notifications`, `worldguard.enabled` | -| `defaults.yml` | Значения по умолчанию для шахт (resources → копируется при первом запуске) | -| `messages.yml` | Все сообщения плагина (`mine-reset-auto`, `mine-created`, `no-permission`, ...) | -| `stats.yml` | Статистика игроков (создаётся при первом сохранении) | -| `mines/_defaults.yml` | defaults для mine.yml (создаётся `DefaultsMerger` при первом запуске) | -| `mines/.yml` | Конфигурация отдельной шахты | - -### `MineConfig` (record) - -``` -MineConfig( - name, String - region, RegionConfig (List regions, volume, contains) - blocks, BlockConfig (Map weights — нормализуются к 1.0, FillMode, MaskConfig) - reset, ResetConfig (Duration interval, percentTrigger, percentEnabled, commands, broadcastMessage) - rewards, RewardConfig (список RewardEntry: chance, preventVanillaDrops, blocks, items, commands) - teleport, TeleportConfig (enabled, location) - ui, UIConfig (actionbar: enabled/format/range, hologram: enabled/height/format, timerFormat) - worldGuard, WorldGuardConfig (region-template, owners, members, flags, protect-on-create) - playerSpawn, PlayerSpawnConfig (enabled, location) -) + public static void reload() { ... } + public static void register(Mine mine) { ... } + public static void unregister(Mine mine) { ... } // удаляет + файл конфига + public static Mine valueOf(String name) { ... } // throws IllegalArgumentException + public static Map getTypes() { ... } +} ``` -Валидация и дефенсивные копии — в компакт-конструкторе каждого record. Парсинг/сохранение — через `ConfigLoader` с подлоадерами по секциям (`loader/block`, `loader/region`, ...). `DefaultsMerger` заполняет отсутствующие ключи из `_defaults.yml`. +### `BlockSetter.java` -Формат локаций в yaml: `мир;x;y;z;yaw;pitch` (yaw/pitch можно опустить). Регионы — пары точек `region.selection.1` + `region.selection.2` = один Cuboid. +```java +public abstract class BlockSetter { + protected final World world; + protected final EnumeratedDistribution distribution; // commons-math3 -### WorldGuard + public abstract void fill(Cuboid cuboid, IntConsumer consumer); + // consumer.accept(blocksPlaced) — вызывается после завершения заполнения +} +``` -`WorldGuardRegionService` создаёт/обновляет/удаляет `ProtectedCuboidRegion` при операциях с шахтой. Имя региона — шаблон `{mine_name}_{random_4}` (см. `RegionTemplateConfig`). Флаги — через `WorldGuardFlagParser` (любые: `block-break=allow`, `pvp=deny`, ...), владельцы/участники — через `WorldGuardMemberHandler`. Всё активируется только если WorldGuard присутствует и `worldguard.enabled: true`. +Выбор реализации в `MineConfigLoader.createBlockSetter()`: +- ключ в `contents` содержит `oraxen:` → `OraxenBlockSetter` +- ключ содержит `itemsadder:` → `ItemsAdderBlockSetter` +- иначе → `BukkitBlockSetter` (vanilla) + +### `MineResetHandler.java` + +```java +public class MineResetHandler { + private final AtomicBoolean running = new AtomicBoolean(false); + + public void reset(boolean silent, BlockSetter placer) { + if (!running.compareAndSet(false, true)) return; // защита от двойного сброса + // для каждого cuboid: placer.fill(cub, callback) + // когда ВСЕ кубоиды завершены (AtomicInteger completedCuboids): + // Scheduler.get(plugin).run(() -> постобработка + running.set(false)) + } +} +``` + +### `MineActionBarHandler.java` + +Каждые 10 тиков отправляет action bar игрокам в радиусе `ACTION_BAR_RANGE²`. + +```java +// Отправка через ActionBar из LoAPI utils +import dev.lolib.utils.ActionBar; + +ActionBar.send(player, StringUtils.format(mine.config.ACTION_BAR, + "notbroken", ..., "total", ..., "percent", ..., "time", ...)); +``` + +### `PlayerStats.java` + +```java +public class PlayerStats { + public final UUID uuid; + public String playerName; + private final AtomicLong totalBlocks = new AtomicLong(0); + private final ConcurrentHashMap mineStats = new ConcurrentHashMap<>(); + // ключ = mineName.toLowerCase() + + public void incrementTotal() { ... } + public void incrementMine(String mineName) { ... } // computeIfAbsent → new AtomicLong + public long getMineStat(String mineName) { ... } + public void resetMine(String mineName) { ... } // вычитает из total, мин 0 + public void setTotalBlocks(long count) { ... } // только для десериализации + public void setMineStat(String mineName, long count) { ... } +} +``` + +### `Selection.java` + +Хранит до 5 пар точек. Ключ в `Map` = `(pair-1)*2 + (first?1:2)`. + +``` +pair=1: pos[1], pos[2] +pair=2: pos[3], pos[4] +... +pair=5: pos[9], pos[10] +currentPair — активная пара (переключается через SelectionPairGui) +``` --- @@ -300,65 +565,181 @@ MineConfig( ``` BlockBreakEvent (MONITOR, ignoreCancelled=true) - └── BlockBreakListener.onBlockBreak - └── mines.findByLocation(location) # Optional - └── m.appliesToBlock(location) # contains + (MASK → позиция в маске) - └── m.onBlockBreak(player, block) - ├── MineBlockHandler.handle - │ ├── blocks.decrementAndGet() - │ ├── MineRewardHandler.checkRewards → items + commands - │ ├── StatsManager.incrementBlocks(uuid, mine) # total + per-mine - │ └── если percentTrigger → mine.reset(false) + └── BlockListener.onBlockBreakEvent() + └── for mine : Mines.getTypes().values() + if mine.containsLocation(block.location): + ├── mine.onBlockBreak(player, block, event) + │ ├── MineBlockHandler: blocks-- + │ ├── MineRewardHandler.getRandomRewards(block) + │ │ └── rand <= chance/100 → reward.execute(player) + │ │ ├── dispatchCommand (от консоли) + │ │ └── player.inventory.addItem(items) + │ └── if blocks/volume*100 <= RESET_PERCENT + │ && Config.RESET_ON_PERCENT_ENABLED → mine.reset() + │ + └── StatsManager.incrementStats(player, mine) + └── playerStats.incrementTotal() + incrementMine(mine.name) ``` --- +## Поток сброса шахты + +``` +mine.reset(silent) + ├── tick = 0 + ├── actionBarHandler.reset() + ├── if resetHandler.isRunning() → return (защита от двойного сброса) + ├── mine.blocks = mine.volume (немедленно, до завершения fill) + └── resetHandler.reset(silent, placer) + ├── running.compareAndSet(false, true) + ├── for each cuboid: + │ placer.fill(cuboid, placed -> { + │ if (++completed >= total): + │ Scheduler.get(plugin).run(() -> { + │ executeResetCommands() + │ sendResetNotifications(silent) + │ teleportPlayers() + │ running.set(false) + │ }) + │ }) + └── после fill → ChunkUtils.refreshChunksDelayed(cuboid, 2L) +``` + +--- + +## Поток загрузки плагина + +``` +LoMinesPlugin.enable() [extends LoPlugin] + ├── LoPlugin.onLoad() → dependencies(manager) // commons-math3, commons-io + ├── loLogger().info(...) + ├── SimpleFeatureFlags → определение активных интеграций + ├── CommandManager(this) + │ .registerTabCompleter("mine", ...) + │ .register(9 классов команд) + ├── Bukkit.registerEvents(new BlockListener(), this) + ├── features.ifEnabled("placeholderapi") → register() + ├── features.ifEnabled("statistics") → new Metrics(this, 20058) + ├── MineTicker.schedule(this) // Scheduler.runTimer(1L, 1L) + ├── SelectionWand.startParticleTask(this) // Scheduler.runTimer(0L, 10L) + ├── NotificationManager.init() + ├── StatsManager.load() + ├── scheduleStatsSave() // Scheduler.runTimerAsync(...) + └── reload() + ├── Config.reload() + └── Mines.reload() +``` + +--- + +## Жизненный цикл Mine-объекта + +``` +new Mine(file, reset=true) + ├── Mines.register(this) + └── reload(reset=true) + ├── config.reload() + ├── cuboids = configLoader.loadCuboids() + ├── volume = configLoader.calculateVolume(cuboids) + ├── blocks = volume + ├── rewardHandler.loadRewards() + ├── placer = configLoader.createBlockSetter(world) + └── if reset: mine.reset() + +Mine.tick() [каждый тик] + ├── tick++ + ├── if tick >= RESET_TICKS → reset(false); tick = 0 + └── actionBarHandler.tick(tick, blocks) + +Mines.unregister(mine) + ├── TYPES.remove(mine.name) + └── mine.config.file.delete() +``` + +--- + +## Конфигурация шахты (MineConfig) + +Файл: `plugins/LoMines/mines/.yml` + +| Поле | Ключ в yaml | Тип | Описание | +|---|---|---|---| +| `DISPLAY_NAME` | `display-name` | String | MiniMessage имя | +| `CONTENTS` | `contents` | Map | блок → вес (нормализует EnumeratedDistribution) | +| `SELECTION_CORNER_1..10` | `selection.1..10` | String | `"world;x;y;z;yaw;pitch"` | +| `RESET_TICKS` | `reset.ticks` | long | тики до автосброса (20 = 1 сек) | +| `RESET_PERCENT` | `reset.percent` | double | % для автосброса по проценту | +| `BROADCAST_RESET` | `broadcast-reset` | int | -2=откл, -1=все, 0=мир, N=радиус | +| `TELEPORT_ON_RESET` | `teleport-on-reset` | int | 0=верхний блок, 1=settp, иное=откл | +| `TELEPORT_LOCATION` | `teleport-location` | String | `"world;x;y;z;yaw;pitch"` | +| `ACTION_BAR_ENABLED` | `actionbar.enabled` | boolean | показывать action bar | +| `ACTION_BAR_RANGE` | `actionbar.range` | int | радиус (проверяется как range²) | +| `TIMER_FORMAT` | `timer-format` | int | 1=digital, 2=short, 3=full | +| ~~`SETTER`~~ | ~~`setter`~~ | ~~String~~ | ~~Удалено (только Bukkit API)~~ | +| `RANDOM_REWARDS` | `random-rewards` | List\ | список наград | +| `RESET_COMMANDS` | `reset-commands` | List\ | команды после сброса | + +### Структура блока награды + +```yaml +- chance: 0.01 + prevent-drops: false + blocks: + - "diamond_ore" + items: + - type: diamond + amount: 1 + name: "Редкий алмаз" + commands: + - "eco give 500" +``` + +--- + +## Интеграции + +### PlaceholderAPI + +| Плейсхолдер | Возвращает | +|---|---| +| `%lomines_percent_%` | `blocks/volume*100` (2 знака) | +| `%lomines_notbroken_%` | `mine.blocks` | +| `%lomines_blocksbroken_%` | `volume - blocks` | +| `%lomines_total_%` | `mine.volume` | +| `%lomines_resettime_%` | время до сброса | +| `%lomines_stats_total%` | `playerStats.totalBlocks` | +| `%lomines_stats_%` | `playerStats.getMineStat(mine)` | +| `%lomines_top__name%` | имя в общем топе | +| `%lomines_top__count%` | блоков в общем топе | +| `%lomines_top___name%` | имя в топе по шахте | +| `%lomines_top___count%` | блоков в топе по шахте | + +### WorldGuard + +`WGRegionManager` создаёт/удаляет WG-регион при операциях с шахтой. +Настройки: `Config.WORLDGUARD_ENABLED`, `WORLDGUARD_AUTO_CREATE_REGIONS`, `WORLDGUARD_REGION_PRIORITY`. + +--- + ## Система статистики ``` -StatsManager (data/stats/service) -├── Map stats # ConcurrentHashMap -├── load() / save() → StatsPersistence # stats.yml (players..total / .mines.) -├── startAutoSave() # async, каждые 5 минут (20 * 60 * 5 тиков) -└── incrementBlocks(uuid, mine) → incrementTotal + incrementMine + leaderboard.invalidateCache() +StatsManager +├── ConcurrentHashMap stats +├── load() ← enable() +├── save() ← по таймеру Scheduler.runTimerAsync + disable() +└── incrementStats(Player, Mine) + ├── if !Config.STATISTICS_ENABLED → return + └── stats.computeIfAbsent(uuid) → incrementTotal() + incrementMine(mine.name) -Leaderboard (data/stats/model) # кэшируемый топ -├── getTopTotal(limit) → кэш + double-checked locking -├── getTopByMine(mine, limit) → без кэша -└── getPosition(uuid) → 1-based позиция +Leaderboard (stateless) +├── getTopTotal(limit) → сортировка по totalBlocks desc +├── getTopByMine(mine, limit) → фильтр + сортировка по getMineStat(mine) desc +├── getPosition(uuid) → 1-based, -1 если нет +└── getPositionByMine(uuid, mine) ``` -`PlayerStats`: `AtomicLong totalBlocks` + `ConcurrentHashMap mineStats`. - ---- - -## Команды - -Корневая команда `/lm` (alias: `lomines`, `mine`, `mines`) — `LmCommand` (`command/LmCommand.java`), единственный `CommandExecutor`+`TabCompleter` из plugin.yml. Диспетчеризация — switch по `args[0]` на под-команды: - -| Под-команда | Класс | Право | -|---|---|---| -| `create`, `delete`, `edit`, `reset`, `reload`, `list` | `AdminCommands` / `MineActionHandler` / `CopyCommand` | `lomines.admin` / `.edit` | -| `info` | `InfoCommand` | `lomines.admin` | -| `regions`, `addregion`, `removeregion` | `RegionCommands` | `lomines.admin` | -| `copy` | `CopyCommand` | `lomines.admin` | -| `maskscan` | `MaskCommands` | `lomines.admin.maskscan` | -| `setteleport`, `setspawn`, `clearspawn` | `TeleportCommands` | `lomines.admin.setteleport` / `.setspawn` | -| `hologram` | `HologramCommands` | `lomines.admin` | -| `stats`, `top` | `StatsCommands` | `lomines.stats` | -| `tp` | `TeleportCommand` | `lomines.use` | -| `wand`, `group`, `help` | `PlayerCommands` | `lomines.admin.wand` / `lomines.use` | - -Tab-complete — `LoMinesTabCompleter` (+ `SubcommandCompleter`, `PermissionPredicate`). Права описаны в `plugin.yml` (группа `lomines.admin` включает все admin-права). - ---- - -## Интеграции - -- **PlaceholderAPI** (`integration/placeholder/LoMinesPlaceholderExpansion.java`): `%lomines_mine__%` (name/blocks/total/percent/percentint/world/remaining/resettime/resetseconds), `%lomines_player_%` (blocksmined/rank), `%lomines_count%`. Регистрируется в `IntegrationManager.initPlaceholderAPI`. -- **WorldGuard** — см. выше. -- **Hologram** (`integration/hologram/`): `HologramManager` управляет жизненным циклом, `HologramProvider` — интерфейс, `provider/DecentHologramsProvider` и `provider/HolographicDisplaysProvider` — реализации, `HologramRenderer` — форматирование строк. - --- ## Принципы @@ -368,18 +749,23 @@ Tab-complete — `LoMinesTabCompleter` (+ `SubcommandCompleter`, `PermissionPred - `BlockSetter` — новые реализации добавляются наследованием (OCP) - Все `BlockSetter` взаимозаменяемы в `MineResetHandler` (LSP) - Handler-классы зависят только от нужного (ISP) -- `Mine` зависит от абстракции `BlockSetter`, не от конкретной реализации (DIP) +- `Mine` зависит от абстракции `BlockSetter`, не от конкретного NMS (DIP) **Ограничения на код:** -- Максимум 150 строк на файл, 30 строк на метод (план) — фактически проверяется Checkstyle (см. `config/checkstyle/checkstyle.xml`) -- Асинхронность — только через `Bukkit.getScheduler()`, никогда `new Thread()` напрямую -- Bukkit API — только в main thread (кроме async-fill и async-save статистики) -- Внутренние зависимости — конструкторы, без static-синглтонов +- Максимум 200 строк на файл (кроме сгенерированных/библиотечных) +- Асинхронность — только через `Scheduler.get(plugin)`, никогда `new Thread()` +- Bukkit API — только в main thread (кроме async-save статистики) +- Изменяемое static состояние — только в `Mines.TYPES` и `StatsManager.stats` --- -## Тесты +## Известные проблемы -JUnit 5 + Kotest + Mockito (`src/test/java`), 31 файл, все ≤150 строк. Покрывают: `Mines` (registry/lifecycle/loading), `Mine`/`MineTicker`, конфиги (`MineConfig`, `BlockConfig`, `BlockKey`, `ResetConfig`, `UIConfig`), статистику/лидерборды, утилиты (LocationParser, TimeFormatter, ChunkUtils, Selection), команды. - -> Часть тестов отключена (`@Disabled`) — требуют Paper `RegistryAccess` или WorldGuard, недоступных в unit-среде. Запуск: `./gradlew test`. +| # | Проблема | Файл | Приоритет | +|---|---|---|---| +| 1 | `Leaderboard.getTopTotal()` — O(n log n) без кэша | `Leaderboard.java` | средний | +| 2 | `mine.blocks = mine.volume` выставляется до завершения fill — ложный процент-reset во время сброса | `Mine.java` | средний | +| 3 | `MineResetHandler.reset()` принимает `BlockSetter` параметром — лишняя связность | `MineResetHandler.java` | низкий | +| 4 | Нет тестов на `MineResetHandler`, `MineBlockHandler`, `BlockListener`, `Mines` | `test/` | высокий | +| 5 | `ChunkUtils` определяет Paper через `Class.forName` — хрупко | `ChunkUtils.java` | низкий | +| 6 | `SelectionWand.drawLine` не ограничивает частицы по Y — спам для высоких шахт | `SelectionWand.java` | низкий | diff --git a/README.md b/README.md index f2719b7..dd1679d 100644 --- a/README.md +++ b/README.md @@ -1,141 +1,219 @@ -
- # LoMines -Multi-region mine management plugin with WorldGuard integration and player statistics for Paper servers. +Advanced mine management plugin for Paper 1.21.4+ -![Java](https://img.shields.io/badge/Java-21+-orange?style=flat-square&logo=openjdk&logoColor=white) -![Paper](https://img.shields.io/badge/Paper-1.19.2+-blue?style=flat-square) -![Folia](https://img.shields.io/badge/Folia-supported-purple?style=flat-square) -![License](https://img.shields.io/badge/license-GPLv3-blue?style=flat-square&logo=gnu&logoColor=white) -![version](https://img.shields.io/badge/version-3.0.0-green?style=flat-square) +## Features -[English](#english) | [Русский](#russian) +- **Multiple fill modes**: Cuboid (fill entire region) or Mask (fill marked positions only) +- **Weighted block system**: Configure different blocks with spawn chances +- **Automatic resets**: Timer-based or percentage-based triggers +- **WorldGuard integration**: Auto-create regions for new mines +- **PlaceholderAPI support**: Display mine info in other plugins +- **GUI editor**: In-game mine configuration editor +- **Player statistics**: Track blocks mined, mines reset, playtime -
+## Commands ---- - - - -## English - -### Overview - -LoMines is an advanced mine management plugin for Paper 1.19.2+ servers. It supports multiple fill modes, weighted block systems, automatic resets, WorldGuard integration, in-game GUI editor, and player statistics. - -### Features - -| Feature | Description | -|---------|-------------| -| Multiple fill modes | Cuboid (fill entire region) or Mask (fill marked positions) | -| Weighted blocks | Configure spawn chances per block type | -| Automatic resets | Timer-based or percentage-based triggers | -| WorldGuard integration | Auto-create regions for new mines | -| GUI editor | In-game mine configuration | -| Player statistics | Track blocks mined, resets, playtime | -| Leaderboards | Top players per mine or global | -| Group wand | Create multiple mines from batch selections | -| PlaceholderAPI | Display mine info in other plugins | -| Holograms | DecentHolograms and HolographicDisplays support | - -### Commands +### Admin Commands | Command | Permission | Description | |---------|------------|-------------| -| `/lm create ` | `lomines.admin` | Create a new mine | -| `/lm delete ` | `lomines.admin` | Delete a mine | -| `/lm reset [silent]` | `lomines.admin` | Reset a mine | -| `/lm reload` | `lomines.admin` | Reload configs | -| `/lm list` | `lomines.admin` | List all mines | -| `/lm edit ` | `lomines.admin.edit` | Open GUI editor | -| `/lm wand` | `lomines.admin.wand` | Get selection wand | -| `/lm group ` | `lomines.admin.wand` | Batch mine creation | -| `/lm setteleport ` | `lomines.admin.setteleport` | Set teleport point | -| `/lm setspawn ` | `lomines.admin.setspawn` | Set safe spawn | -| `/lm maskscan ` | `lomines.admin.maskscan` | Scan mask markers | -| `/lm stats [player]` | `lomines.stats` | View statistics | +| `/lm create ` | `lomines.admin.create` | Create a new mine | +| `/lm delete ` | `lomines.admin.delete` | Delete a mine | +| `/lm reset [silent]` | `lomines.admin.reset` | Reset a mine manually | +| `/lm reload` | `lomines.admin.reload` | Reload all mine configs | +| `/lm list` | `lomines.admin.list` | List all mines | +| `/lm edit ` | `lomines.admin.edit` | Open GUI editor for a mine | +| `/lm setteleport ` | `lomines.admin.setteleport` | Set teleport location (your current position) | +| `/lm setspawn ` | `lomines.admin.setspawn` | Set safe spawn location for stuck players | +| `/lm clearspawn ` | `lomines.admin.setspawn` | Clear spawn location | +| `/lm maskscan ` | `lomines.admin.maskscan` | Scan mask marker blocks | + +### Player Commands + +| Command | Permission | Description | +|---------|------------|-------------| +| `/lm stats [player]` | `lomines.stats` | View mining statistics | | `/lm top [mine] [limit]` | `lomines.stats` | View leaderboard | -### Placeholders +### Wand Commands -| Placeholder | Description | -|-------------|-------------| -| `%lomines_mine__blocks%` | Current blocks in mine | -| `%lomines_mine__percent%` | Fill percentage | -| `%lomines_mine__remaining%` | Remaining blocks | -| `%lomines_mine__resettime%` | Time since reset | -| `%lomines_player_blocksmined%` | Total blocks mined | -| `%lomines_player_rank%` | Leaderboard rank | -| `%lomines_count%` | Total mines count | +| Command | Permission | Description | +|---------|------------|-------------| +| `/lm wand` | `lomines.admin.wand` | Get the region selection wand | +| `/lm group ` | `lomines.admin.wand` | Create mines from group wand selections | -### Dependencies +## Permissions -- Required: Paper 1.19.2+, Java 21+ -- Optional: PlaceholderAPI, WorldGuard, DecentHolograms, HolographicDisplays, Oraxen, ItemsAdder +```yaml +lomines.admin: + description: All admin commands + default: op + children: + lomines.use: true + lomines.admin.create: true + lomines.admin.delete: true + lomines.admin.reset: true + lomines.admin.reload: true + lomines.admin.list: true + lomines.admin.edit: true + lomines.admin.setteleport: true + lomines.admin.setspawn: true + lomines.admin.maskscan: true + lomines.admin.wand: true + lomines.stats: true + lomines.stats.others: true -### Installation +lomines.use: + description: Basic usage + default: true -1. Drop the jar into `plugins/` -2. Restart the server -3. Configure `plugins/LoMines/mines/` and `plugins/LoMines/defaults.yml` +lomines.stats: + description: View own stats and leaderboards + default: true ---- +lomines.stats.others: + description: View other players' stats + default: op +``` - +## Placeholders (PlaceholderAPI) -## Русский +### Mine Information -### Обзор +| Placeholder | Description | Example | +|-------------|-------------|---------| +| `%lomines_mine__name%` | Mine name | `mymine` | +| `%lomines_mine__blocks%` | Current blocks | `8542` | +| `%lomines_mine__total%` | Total volume | `10000` | +| `%lomines_mine__percent%` | Fill percentage (1 decimal) | `85.4` | +| `%lomines_mine__percentint%` | Fill percentage (integer) | `85` | +| `%lomines_mine__world%` | World name | `world` | +| `%lomines_mine__remaining%` | Remaining blocks | `1458` | +| `%lomines_mine__resettime%` | Time since reset (mm:ss) | `03:45` | +| `%lomines_mine__resetseconds%` | Seconds since reset | `225` | -LoMines - продвинутый плагин управления шахтами для Paper 1.19.2+ серверов. Поддерживает несколько режимов заполнения, взвешенные блоки, автоматические сбросы, интеграцию с WorldGuard, GUI-редактор и статистику игроков. +### Player Statistics -### Возможности +| Placeholder | Description | Example | +|-------------|-------------|---------| +| `%lomines_player_blocksmined%` | Total blocks mined | `15234` | +| `%lomines_player_minesreset%` | Mines reset count | `45` | +| `%lomines_player_playtime%` | Formatted play time | `2h 15m` | +| `%lomines_player_rank%` | Leaderboard rank | `3` | -| Возможность | Описание | -|-------------|----------| -| Режимы заполнения | Cuboid (весь регион) или Mask (по меткам) | -| Взвешенные блоки | Настройка шансов появления для каждого блока | -| Авто-сбросы | По таймеру или по проценту добычи | -| WorldGuard | Авто-создание регионов для новых шахт | -| GUI-редактор | Настройка шахт прямо в игре | -| Статистика | Добыто блоков, сбросы, время игры | -| Лидерборды | Топ игроков по шахте или глобально | -| Групповая палка | Создание нескольких шахт за раз | -| PlaceholderAPI | Информация о шахтах в других плагинах | -| Голограммы | Поддержка DecentHolograms и HolographicDisplays | +### Global -### Команды +| Placeholder | Description | Example | +|-------------|-------------|---------| +| `%lomines_count%` | Total number of mines | `12` | -| Команда | Право | Описание | -|---------|-------|----------| -| `/lm create <имя>` | `lomines.admin` | Создать шахту | -| `/lm delete <имя>` | `lomines.admin` | Удалить шахту | -| `/lm reset <имя>` | `lomines.admin` | Сбросить шахту | -| `/lm edit <имя>` | `lomines.admin.edit` | Открыть GUI-редактор | -| `/lm wand` | `lomines.admin.wand` | Получить палку выделения | -| `/lm stats [игрок]` | `lomines.stats` | Статистика | -| `/lm top [шахта]` | `lomines.stats` | Лидерборд | +## Configuration -### Зависимости +### defaults.yml -- Обязательные: Paper 1.19.2+, Java 21+ -- Опциональные: PlaceholderAPI, WorldGuard, DecentHolograms, Oraxen, ItemsAdder +Default configuration applied to all mines: -### Установка +```yaml +# Reset settings +reset: + interval: "5m" # Reset interval (30s, 5m, 2h, 1d) + percent-trigger: 10.0 # Trigger reset when X% of blocks mined + percent-enabled: false # Enable percentage trigger + commands: [] # Commands to run on reset + broadcast: "" # Broadcast message on reset -1. Положите jar в папку `plugins/` -2. Перезапустите сервер -3. Настройте `plugins/LoMines/mines/` и `plugins/LoMines/defaults.yml` +# Block settings +blocks: + fill-mode: CUBOID # CUBOID or MASK + mask: + marker: "pink_concrete" # Marker block for mask mode ---- +# Teleport settings +teleport: + enabled: false + location: "" -### Links +# Safe spawn for stuck players +player-spawn: + enabled: false + location: "" -- [Releases](../../releases) -- [Issues](../../issues) -- [License](LICENSE) +# UI settings +ui: + actionbar: + enabled: true + format: "{mine} {percent}% ({time})" + range: 50.0 -### License +# WorldGuard integration +worldguard: + enabled: false + region-template: "{mine_name}_{random_4}" + owners: [] + members: [] + flags: + - "passthrough=deny" + - "build=allow" +``` -GNU General Public License v3.0 \ No newline at end of file +## Usage Examples + +### Creating a Simple Mine + +```bash +# 1. Get the wand +/lm wand + +# 2. Select two corners (left and right click) +# 3. Create the mine +/lm create mymine + +# 4. Edit the config file or use GUI +/lm edit mymine +``` + +### Setting Up Teleport + +```bash +# Stand where players should be teleported +/lm setteleport mymine + +# Stand where STUCK players should spawn (optional, falls back to teleport location) +/lm setspawn mymine +``` + +### Using Mask Fill Mode + +```bash +# 1. Paint positions with pink_concrete (or configured marker) +# 2. Scan the markers +/lm maskscan mymine + +# Mine will now only fill at marked positions +``` + +### WorldGuard Auto-Regions + +Enable in `plugins/LoMines/mines/_defaults.yml`: + +```yaml +worldguard: + enabled: true + region-template: "mine_{mine_name}_{random_4}" + flags: + - "passthrough=deny" + - "build=allow" +``` + +When you create a mine, a WorldGuard region `mine_mymine_7392` is automatically created. + +## Support + +- Paper 1.21.4+ +- Java 21+ +- Optional: PlaceholderAPI, WorldGuard + +## License + +MIT License diff --git a/build.gradle.kts b/build.gradle.kts index 1dd9a63..0caa85a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,6 @@ plugins { java id("com.github.johnrengelman.shadow") version "8.1.1" - checkstyle } group = "com.loki" @@ -14,88 +13,79 @@ java { } } -tasks.withType { - options.release.set(21) -} - repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") - maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") - maven("https://maven.enginehub.org/repo/") - maven("https://repo.codemc.io/repository/maven-public/") maven("https://jitpack.io") } -dependencies { - compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") - implementation("org.apache.commons:commons-math3:3.6.1") - implementation("commons-io:commons-io:2.18.0") - compileOnly("me.clip:placeholderapi:2.11.6") - compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.13") - compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10") +// LoAPI: libs/lolib*.jar — fileTree индексируется Gradle / Java LS стабильнее, чем files(singleJar) +val lolibTree = fileTree("libs") { include("lolib*.jar") } +if (lolibTree.files.isEmpty()) { + throw GradleException( + "Missing libs/lolib*.jar — add LoAPI fat jar to ${rootProject.file("libs").absolutePath}" + ) +} +dependencies { + // Paper API 1.21.4 (includes Adventure API) + compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + + // LoAPI (local) + implementation(lolibTree) + + // Runtime dependencies (loaded via DependencyManager from LoAPI) + // These are marked as compileOnly because they will be downloaded at runtime + compileOnly("org.apache.commons:commons-math3:3.6.1") + compileOnly("commons-io:commons-io:2.18.0") + + // Adventure API (included in Paper, but explicit for clarity) + compileOnly("net.kyori:adventure-api:4.17.0") + compileOnly("net.kyori:adventure-text-minimessage:4.17.0") + + // Optional plugin integrations (soft dependencies) + // compileOnly("com.github.oraxen:oraxen:1.180.0") + // compileOnly("com.github.LoneDev6:api-itemsadder:3.6.4") + + // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") testImplementation("io.kotest:kotest-runner-junit5:5.9.1") testImplementation("io.kotest:kotest-assertions-core:5.9.1") testImplementation("io.kotest:kotest-property:5.9.1") testImplementation("org.mockito:mockito-core:5.14.2") - testImplementation("org.mockito:mockito-inline:5.2.0") - testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.110.0") { - exclude(group = "org.junit.jupiter", module = "junit-jupiter-api") - } - testImplementation("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") - testImplementation("net.kyori:adventure-api:4.17.0") - testImplementation("net.kyori:adventure-text-minimessage:4.17.0") - testImplementation("com.sk89q.worldguard:worldguard-bukkit:7.0.13") -} - -checkstyle { - toolVersion = "10.21.4" - configFile = file("config/checkstyle/checkstyle.xml") - isIgnoreFailures = false - maxErrors = 0 - maxWarnings = 0 -} - -tasks.withType().configureEach { - reports { - xml.required.set(true) - html.required.set(true) - } + testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + testImplementation("org.apache.commons:commons-math3:3.6.1") + testImplementation("commons-io:commons-io:2.18.0") } tasks { - shadowJar { archiveClassifier.set("") - - // Temporary: disable relocate to avoid ASM Java 21 bytecode issues - // relocate( - // "dev.loki.lomines.libs.lolib", - // "com.loki.lomines.libs.lolib" - // ) + + // Relocate LoAPI to avoid conflicts + relocate("dev.loki.lomines.libs.lolib", "com.loki.lomines.libs.lolib") + + // Don't minimize - causes issues with Java 21 + // minimize() } - + build { dependsOn(shadowJar) } - + test { useJUnitPlatform() } - + processResources { val props = mapOf( "version" to version, "description" to description ) - inputs.properties(props) filteringCharset = "UTF-8" - filesMatching("plugin.yml") { expand(props) } } -} \ No newline at end of file +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml deleted file mode 100644 index 946b028..0000000 --- a/config/checkstyle/checkstyle.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55..63f2184 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew deleted file mode 100755 index 23d15a9..0000000 --- a/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH="\\\"\\\"" - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index db3a6ac..c8f7d56 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,7 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - +@rem Gradle startup script for Windows @if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +java -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* diff --git a/libs/.gitkeep b/libs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/dev/loki/lomines/ComponentInitializer.java b/src/main/java/dev/loki/lomines/ComponentInitializer.java index 42a9dc7..eb5952b 100644 --- a/src/main/java/dev/loki/lomines/ComponentInitializer.java +++ b/src/main/java/dev/loki/lomines/ComponentInitializer.java @@ -1,11 +1,11 @@ package dev.loki.lomines; -import dev.loki.lomines.core.mine.service.MineTicker; -import dev.loki.lomines.core.mine.registry.Mines; -import dev.loki.lomines.data.stats.service.StatsManager; +import dev.loki.lomines.core.mine.MineTicker; +import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.wand.group.GroupWandManager; - +import dev.lolib.commands.CommandManager; import java.io.IOException; import java.nio.file.Files; @@ -28,7 +28,7 @@ final class ComponentInitializer { if (!Files.exists(minesFolder)) { Files.createDirectories(minesFolder); - plugin.getLogger().info("Created mines directory"); + plugin.loLogger().info("Created mines directory"); } } @@ -36,19 +36,20 @@ final class ComponentInitializer { Mines mines = new Mines(plugin); GroupWandManager groupWandManager = new GroupWandManager(); StatsManager statsManager = new StatsManager(plugin); + CommandManager commandManager = new CommandManager(plugin); IntegrationManager integrationManager = new IntegrationManager(plugin); - plugin.getLogger().info("Core components initialized"); + plugin.loLogger().info("Core components initialized"); - return new Components(mines, groupWandManager, statsManager, integrationManager); + return new Components(mines, groupWandManager, statsManager, commandManager, integrationManager); } void loadMines(Mines mines) { try { mines.loadAll(); - plugin.getLogger().info("Loaded " + mines.getAll().size() + " mine(s)"); + plugin.loLogger().info("Loaded " + mines.getAll().size() + " mine(s)"); } catch (IOException e) { - plugin.getLogger().severe("Failed to load mines: " + e.getMessage()); + plugin.loLogger().error("Failed to load mines: " + e.getMessage()); e.printStackTrace(); } } @@ -64,9 +65,9 @@ final class ComponentInitializer { try { statsManager.load(); statsManager.startAutoSave(); - plugin.getLogger().info("Statistics system enabled"); + plugin.loLogger().info("Statistics system enabled"); } catch (IOException e) { - plugin.getLogger().warning("Failed to load statistics: " + e.getMessage()); + plugin.loLogger().warn("Failed to load statistics: " + e.getMessage()); } } } @@ -75,6 +76,7 @@ final class ComponentInitializer { Mines mines, GroupWandManager groupWandManager, StatsManager statsManager, + CommandManager commandManager, IntegrationManager integrationManager ) { } diff --git a/src/main/java/dev/loki/lomines/LoMinesPlugin.java b/src/main/java/dev/loki/lomines/LoMinesPlugin.java index e6d46f9..b80a7ec 100644 --- a/src/main/java/dev/loki/lomines/LoMinesPlugin.java +++ b/src/main/java/dev/loki/lomines/LoMinesPlugin.java @@ -1,26 +1,33 @@ package dev.loki.lomines; -import dev.loki.lomines.core.mine.service.MineTicker; -import dev.loki.lomines.core.mine.registry.Mines; -import dev.loki.lomines.data.stats.service.StatsManager; +import dev.loki.lomines.core.mine.MineTicker; +import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.data.stats.StatsManager; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.hologram.HologramManager; import dev.loki.lomines.wand.WandParticleService; import dev.loki.lomines.wand.group.GroupWandManager; -import org.bukkit.plugin.java.JavaPlugin; +import dev.lolib.commands.CommandManager; +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 MineTicker mineTicker; private GroupWandManager groupWandManager; private StatsManager statsManager; + private CommandManager commandManager; private IntegrationManager integrationManager; private WandParticleService wandParticleService; private HologramManager hologramManager; @Override - public void onEnable() { + protected void enable() { try { saveDefaultConfig(); @@ -31,6 +38,7 @@ public final class LoMinesPlugin extends JavaPlugin { this.mines = components.mines(); this.groupWandManager = components.groupWandManager(); this.statsManager = components.statsManager(); + this.commandManager = components.commandManager(); this.integrationManager = components.integrationManager(); this.wandParticleService = new WandParticleService(this); this.hologramManager = new HologramManager(this); @@ -39,51 +47,65 @@ public final class LoMinesPlugin extends JavaPlugin { this.mineTicker = initializer.startTicker(mines); RegistrationManager registrationManager = new RegistrationManager(this); - registrationManager.registerCommands(); + registrationManager.registerCommands(commandManager); + registrationManager.registerTabCompleter(); registrationManager.registerListeners(); registrationManager.initializeIntegrations(integrationManager); initializer.startStatistics(statsManager); - getLogger().info("LoMines has been enabled!"); + loLogger().info("LoMines has been enabled!"); } catch (Exception e) { - getLogger().severe("Failed to enable LoMines: " + e.getMessage()); + loLogger().error("Failed to enable LoMines: " + e.getMessage()); e.printStackTrace(); getServer().getPluginManager().disablePlugin(this); } } @Override - public void onDisable() { + protected void disable() { try { if (mineTicker != null) { mineTicker.stop(); mineTicker = null; } + if (mines != null) { mines.getAll().forEach(mine -> mine.stop()); } + if (statsManager != null) { statsManager.stopAutoSave(); statsManager.save(); - getLogger().info("Statistics saved successfully"); + loLogger().info("Statistics saved successfully"); } + if (integrationManager != null) { integrationManager.shutdown(); } + if (wandParticleService != null) { wandParticleService.stopAll(); } + if (hologramManager != null) { hologramManager.shutdown(); } - getLogger().info("LoMines has been disabled!"); + + loLogger().info("LoMines has been disabled!"); } catch (Exception e) { - getLogger().severe("Error during plugin shutdown: " + e.getMessage()); + loLogger().error("Error during plugin shutdown: " + e.getMessage()); 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() { return mines; } @@ -96,6 +118,10 @@ public final class LoMinesPlugin extends JavaPlugin { return statsManager; } + public CommandManager getCommandManager() { + return commandManager; + } + public IntegrationManager getIntegrationManager() { return integrationManager; } diff --git a/src/main/java/dev/loki/lomines/RegistrationManager.java b/src/main/java/dev/loki/lomines/RegistrationManager.java index 32b878e..d8e7436 100644 --- a/src/main/java/dev/loki/lomines/RegistrationManager.java +++ b/src/main/java/dev/loki/lomines/RegistrationManager.java @@ -1,13 +1,22 @@ package dev.loki.lomines; -import dev.loki.lomines.command.LmCommand; +import dev.loki.lomines.command.admin.AdminCommands; +import dev.loki.lomines.command.admin.HologramCommands; +import dev.loki.lomines.command.admin.MaskCommands; +import dev.loki.lomines.command.admin.StatsCommands; +import dev.loki.lomines.command.common.LoMinesTabCompleter; +import dev.loki.lomines.command.player.PlayerCommands; import dev.loki.lomines.integration.IntegrationManager; -import dev.loki.lomines.listener.block.BlockBreakListener; -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; +import dev.loki.lomines.listener.BlockBreakListener; +import dev.loki.lomines.listener.GroupGuiListener; +import dev.loki.lomines.listener.MineEditGuiListener; +import dev.loki.lomines.listener.PlayerInteractListener; +import dev.loki.lomines.listener.PlayerJoinListener; +import dev.lolib.commands.CommandManager; +/** + * Handles registration of commands, listeners, and integrations. + */ final class RegistrationManager { private final LoMinesPlugin plugin; @@ -16,16 +25,24 @@ final class RegistrationManager { this.plugin = plugin; } - void registerCommands() { - LmCommand executor = new LmCommand(plugin); - for (String alias : new String[]{"lm", "lomines", "mine", "mines"}) { - var cmd = plugin.getCommand(alias); - if (cmd != null) { - cmd.setExecutor(executor); - cmd.setTabCompleter(executor); - } - } - plugin.getLogger().info("Commands registered"); + void registerCommands(CommandManager commandManager) { + commandManager.register(new AdminCommands(plugin)); + commandManager.register(new PlayerCommands(plugin)); + commandManager.register(new StatsCommands(plugin)); + commandManager.register(new MaskCommands(plugin)); + commandManager.register(new HologramCommands(plugin)); + + plugin.loLogger().info("Commands registered"); + } + + void registerTabCompleter() { + LoMinesTabCompleter tabCompleter = new LoMinesTabCompleter(plugin); + plugin.getCommand("lm").setTabCompleter(tabCompleter); + plugin.getCommand("lomines").setTabCompleter(tabCompleter); + plugin.getCommand("mine").setTabCompleter(tabCompleter); + plugin.getCommand("mines").setTabCompleter(tabCompleter); + + plugin.loLogger().info("Tab completer registered"); } void registerListeners() { @@ -37,11 +54,12 @@ final class RegistrationManager { if (plugin.getConfig().getBoolean("statistics-enabled", true)) { plugin.getServer().getPluginManager().registerEvents(new PlayerJoinListener(plugin), plugin); } - plugin.getLogger().info("Event listeners registered"); + + plugin.loLogger().info("Event listeners registered"); } void initializeIntegrations(IntegrationManager integrationManager) { integrationManager.initAll(); - plugin.getLogger().info("Integrations initialized"); + plugin.loLogger().info("Integrations initialized"); } } diff --git a/src/main/java/dev/loki/lomines/block/BlockSetter.java b/src/main/java/dev/loki/lomines/block/BlockSetter.java index be5fcb0..537ac5a 100644 --- a/src/main/java/dev/loki/lomines/block/BlockSetter.java +++ b/src/main/java/dev/loki/lomines/block/BlockSetter.java @@ -1,6 +1,6 @@ package dev.loki.lomines.block; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.Cuboid; import org.bukkit.Location; import java.util.List; diff --git a/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java b/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java index 2c93c22..e336bcd 100644 --- a/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java +++ b/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java @@ -3,8 +3,8 @@ package dev.loki.lomines.block; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.util.block.BlockUpdateUtil; -import dev.loki.lomines.util.location.geo.Cuboid; - +import dev.loki.lomines.util.location.Cuboid; +import dev.lolib.scheduler.Scheduler; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -35,11 +35,11 @@ public final class BukkitBlockSetter extends BlockSetter { @Override public void fill(Cuboid region, IntConsumer callback) { - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Scheduler.get(plugin).runAsync(() -> { int count = fillSync(region); // Send block updates on main thread to prevent ghost blocks - Bukkit.getScheduler().runTask(plugin, () -> { + Scheduler.get(plugin).run(() -> { BlockUpdateUtil.sendRegionUpdate( region.getWorld(), region.getMinX(), region.getMinY(), region.getMinZ(), @@ -53,14 +53,14 @@ public final class BukkitBlockSetter extends BlockSetter { @Override public void fillAtLocations(List locations, IntConsumer callback) { if (locations == null || locations.isEmpty()) { - Bukkit.getScheduler().runTask(plugin, () -> callback.accept(0)); + Scheduler.get(plugin).run(() -> callback.accept(0)); return; } - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Scheduler.get(plugin).runAsync(() -> { int count = fillAtLocationsSync(locations); // Send block updates on main thread to prevent ghost blocks - Bukkit.getScheduler().runTask(plugin, () -> { + Scheduler.get(plugin).run(() -> { BlockUpdateUtil.sendLocationsUpdate(locations); callback.accept(count); }); @@ -106,8 +106,8 @@ public final class BukkitBlockSetter extends BlockSetter { currentWeight += entry.getValue(); if (random <= currentWeight) { // BlockKey is already validated to be a block - if (entry.getKey() instanceof BlockKey.Vanilla(Material material)) { - return Bukkit.createBlockData(material); + if (entry.getKey() instanceof BlockKey.Vanilla vanilla) { + return Bukkit.createBlockData(vanilla.material()); } // Fallback for non-vanilla keys in vanilla setter (shouldn't happen) return Bukkit.createBlockData(Material.STONE); @@ -116,8 +116,8 @@ public final class BukkitBlockSetter extends BlockSetter { // Fallback to first entry BlockKey firstKey = weights.keySet().iterator().next(); - if (firstKey instanceof BlockKey.Vanilla(Material material)) { - return Bukkit.createBlockData(material); + if (firstKey instanceof BlockKey.Vanilla vanilla) { + return Bukkit.createBlockData(vanilla.material()); } return Bukkit.createBlockData(Material.STONE); } diff --git a/src/main/java/dev/loki/lomines/command/LmCommand.java b/src/main/java/dev/loki/lomines/command/LmCommand.java deleted file mode 100644 index edcde6c..0000000 --- a/src/main/java/dev/loki/lomines/command/LmCommand.java +++ /dev/null @@ -1,85 +0,0 @@ -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 onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { - return tabCompleter.onTabComplete(sender, command, alias, args); - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/manage/MineActionHandler.java b/src/main/java/dev/loki/lomines/command/admin/AdminCommands.java similarity index 52% rename from src/main/java/dev/loki/lomines/command/admin/manage/MineActionHandler.java rename to src/main/java/dev/loki/lomines/command/admin/AdminCommands.java index 261883d..5560b01 100644 --- a/src/main/java/dev/loki/lomines/command/admin/manage/MineActionHandler.java +++ b/src/main/java/dev/loki/lomines/command/admin/AdminCommands.java @@ -1,31 +1,45 @@ -package dev.loki.lomines.command.admin.manage; +package dev.loki.lomines.command.admin; 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.core.mine.Mine; +import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.gui.mine.main.MineEditGui; import dev.loki.lomines.util.ErrorHandler; 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.format.NamedTextColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; 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 Mines mines; private final ErrorHandler errorHandler; - public MineActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { + public AdminCommands(LoMinesPlugin plugin) { this.plugin = plugin; - this.mines = mines; - this.errorHandler = errorHandler; + this.mines = plugin.getMines(); + this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); } - public void create(CommandSender sender, String name) { + /** + * Creates a new mine with the given name. + * Usage: /lm create + */ + @Subcommand(value = "create", permission = "lomines.admin.create") + public void create(CommandSender sender, @Arg("name") String name) { try { mines.create(name); sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name)); @@ -44,7 +58,12 @@ public final class MineActionHandler { } } - public void delete(CommandSender sender, String name) { + /** + * Deletes an existing mine. + * Usage: /lm delete + */ + @Subcommand(value = "delete", permission = "lomines.admin.delete") + public void delete(CommandSender sender, @Arg("name") String name) { try { mines.delete(name); sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name)); @@ -56,7 +75,12 @@ public final class MineActionHandler { } } - public void edit(CommandSender sender, String mineName) { + /** + * Opens the GUI editor for a mine. + * Usage: /lm edit + */ + @Subcommand(value = "edit", permission = "lomines.admin.edit") + public void edit(CommandSender sender, @Arg("mine") String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -71,7 +95,16 @@ public final class MineActionHandler { 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 [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 { Mine mine = mines.get(mineName); mine.reset(silent); @@ -81,6 +114,11 @@ public final class MineActionHandler { } } + /** + * Reloads all mine configurations from disk. + * Usage: /lm reload + */ + @Subcommand(value = "reload", permission = "lomines.admin.reload") public void reload(CommandSender sender) { try { mines.getAll().forEach(Mine::stop); @@ -92,4 +130,32 @@ public final class MineActionHandler { "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 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); + } } diff --git a/src/main/java/dev/loki/lomines/command/admin/stats/HologramCommands.java b/src/main/java/dev/loki/lomines/command/admin/HologramCommands.java similarity index 58% rename from src/main/java/dev/loki/lomines/command/admin/stats/HologramCommands.java rename to src/main/java/dev/loki/lomines/command/admin/HologramCommands.java index abb9a20..57ee1ce 100644 --- a/src/main/java/dev/loki/lomines/command/admin/stats/HologramCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/HologramCommands.java @@ -1,30 +1,35 @@ -package dev.loki.lomines.command.admin.stats; +package dev.loki.lomines.command.admin; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.integration.hologram.HologramManager; +import dev.lolilb.commands.annotation.Arg; +import dev.lolilb.commands.annotation.Subcommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; -public class HologramCommands { +/** + * Commands for managing holograms above mines. + */ +class HologramCommands { private final LoMinesPlugin plugin; private final HologramManager hologramManager; - public HologramCommands(LoMinesPlugin plugin) { + HologramCommands(LoMinesPlugin plugin) { this.plugin = plugin; this.hologramManager = plugin.getHologramManager(); } - public void handle(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm hologram [on|off]", NamedTextColor.RED)); - return; - } - String mineName = args[0]; - String state = args.length > 1 ? args[1] : null; - + /** + * Toggles hologram for a mine. + * Usage: /lm hologram [on|off] + */ + @Subcommand(value = "hologram", permission = "lomines.admin.hologram") + void hologram(CommandSender sender, @Arg("mine") String mineName, + @Arg(value = "state", optional = true) String state) { Mine mine = plugin.getMines().find(mineName).orElse(null); if (mine == null) { sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); @@ -38,9 +43,10 @@ public class HologramCommands { boolean enable; if (state == null) { + // Toggle enable = hologramManager.toggleHologram(mine); } else { - enable = "on".equalsIgnoreCase(state) || "true".equalsIgnoreCase(state) || "enable".equalsIgnoreCase(state); + enable = state.equalsIgnoreCase("on") || state.equalsIgnoreCase("true") || state.equalsIgnoreCase("enable"); if (enable) { hologramManager.createMineHologram(mine); } else { @@ -49,6 +55,6 @@ public class HologramCommands { } sender.sendMessage(Component.text( - "Голограмма для шахты " + mineName + " " + (enable ? "§aвключена" : "§cвыключена"))); + "Голограмма для шахты " + mineName + " " + (enable ? "§aвключена" : "§cвыключена"))); } } diff --git a/src/main/java/dev/loki/lomines/command/admin/misc/MaskCommands.java b/src/main/java/dev/loki/lomines/command/admin/MaskCommands.java similarity index 64% rename from src/main/java/dev/loki/lomines/command/admin/misc/MaskCommands.java rename to src/main/java/dev/loki/lomines/command/admin/MaskCommands.java index d647610..c91fbe5 100644 --- a/src/main/java/dev/loki/lomines/command/admin/misc/MaskCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/MaskCommands.java @@ -1,16 +1,23 @@ -package dev.loki.lomines.command.admin.misc; +package dev.loki.lomines.command.admin; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.registry.Mines; -import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.util.ErrorHandler; import dev.loki.lomines.util.MessageFormatter; - -import org.bukkit.Bukkit; +import dev.lolib.commands.annotation.Arg; +import dev.lolib.commands.annotation.Command; +import dev.lolib.commands.annotation.Subcommand; +import dev.lolib.scheduler.Scheduler; import org.bukkit.command.CommandSender; 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 { private final LoMinesPlugin plugin; @@ -23,13 +30,9 @@ public class MaskCommands { this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); } - public void handle(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(MessageFormatter.error("Usage: /lm maskscan ")); - return; - } - String name = args[0]; - Bukkit.getScheduler().runTask(plugin, () -> { + @Subcommand(value = "maskscan", permission = "lomines.admin.maskscan") + public void maskScan(CommandSender sender, @Arg("mine") String name) { + Scheduler.get(plugin).run(() -> { try { int n = mines.scanAndSaveMask(name); sender.sendMessage(MessageFormatter.success( diff --git a/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java b/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java new file mode 100644 index 0000000..d6d293a --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java @@ -0,0 +1,186 @@ +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 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 byMine = ps.getMineStatsSnapshot(); + if (byMine.isEmpty()) { + return; + } + sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY)); + byMine.entrySet().stream() + .sorted(Map.Entry.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 rows = lb.getTopByMine(key, limit); + sender.sendMessage(Component.text("=== Топ по шахте «" + key + "» ===", NamedTextColor.GOLD)); + sendTopRows(sender, rows); + return; + } + List 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; + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java b/src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java similarity index 56% rename from src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java rename to src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java index dd974d5..5bfdb74 100644 --- a/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java +++ b/src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java @@ -1,32 +1,42 @@ -package dev.loki.lomines.command.admin.misc; +package dev.loki.lomines.command.admin; 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.config.model.MineConfig; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.util.ErrorHandler; 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.format.NamedTextColor; import org.bukkit.Location; import org.bukkit.command.CommandSender; 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 Mines mines; private final ErrorHandler errorHandler; - public TeleportActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { + TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { this.plugin = plugin; this.mines = mines; this.errorHandler = errorHandler; } - public void setTeleport(CommandSender sender, String mineName) { + /** + * Sets the teleport location for players when mine resets. + * Usage: /lm setteleport + */ + @Subcommand(value = "setteleport", permission = "lomines.admin.setteleport") + void setTeleport(CommandSender sender, @Arg("mine") String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -40,7 +50,18 @@ public final class TeleportActionHandler { } var config = mine.getConfig(); - var updatedConfig = withTeleport(config, TeleportConfig.at(player.getLocation())); + var newTeleport = TeleportConfig.at(player.getLocation()); + + var updatedConfig = MineConfig.builder(mineName) + .region(config.region()) + .blocks(config.blocks()) + .reset(config.reset()) + .rewards(config.rewards()) + .teleport(newTeleport) + .ui(config.ui()) + .worldGuard(config.worldGuard()) + .playerSpawn(config.playerSpawn()) + .build(); plugin.getMines().getFileManager().saveConfig(updatedConfig); sender.sendMessage(MessageFormatter.success("Teleport location for mine '" + mineName + "' set to your current position")); @@ -51,7 +72,12 @@ public final class TeleportActionHandler { } } - public void setSpawn(CommandSender sender, String mineName) { + /** + * Sets the safe spawn location for stuck players after mine reset. + * Usage: /lm setspawn + */ + @Subcommand(value = "setspawn", permission = "lomines.admin.setspawn") + void setSpawn(CommandSender sender, @Arg("mine") String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -65,7 +91,18 @@ public final class TeleportActionHandler { } var config = mine.getConfig(); - var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.at(player.getLocation())); + var newSpawn = PlayerSpawnConfig.at(player.getLocation()); + + var updatedConfig = MineConfig.builder(mineName) + .region(config.region()) + .blocks(config.blocks()) + .reset(config.reset()) + .rewards(config.rewards()) + .teleport(config.teleport()) + .ui(config.ui()) + .worldGuard(config.worldGuard()) + .playerSpawn(newSpawn) + .build(); plugin.getMines().getFileManager().saveConfig(updatedConfig); sender.sendMessage(MessageFormatter.success("Safe spawn location for mine '" + mineName + "' set to your current position")); @@ -77,7 +114,12 @@ public final class TeleportActionHandler { } } - public void clearSpawn(CommandSender sender, String mineName) { + /** + * Clears the spawn location for stuck players. + * Usage: /lm clearspawn + */ + @Subcommand(value = "clearspawn", permission = "lomines.admin.setspawn") + void clearSpawn(CommandSender sender, @Arg("mine") String mineName) { try { Mine mine = mines.find(mineName).orElse(null); if (mine == null) { @@ -86,7 +128,16 @@ public final class TeleportActionHandler { } var config = mine.getConfig(); - var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.disabled()); + var updatedConfig = MineConfig.builder(mineName) + .region(config.region()) + .blocks(config.blocks()) + .reset(config.reset()) + .rewards(config.rewards()) + .teleport(config.teleport()) + .ui(config.ui()) + .worldGuard(config.worldGuard()) + .playerSpawn(PlayerSpawnConfig.disabled()) + .build(); plugin.getMines().getFileManager().saveConfig(updatedConfig); sender.sendMessage(MessageFormatter.success("Spawn location for mine '" + mineName + "' cleared")); @@ -97,33 +148,7 @@ public final class TeleportActionHandler { } } - private MineConfig withTeleport(MineConfig config, TeleportConfig teleport) { - return MineConfig.builder(config.name()) - .region(config.region()) - .blocks(config.blocks()) - .reset(config.reset()) - .rewards(config.rewards()) - .teleport(teleport) - .ui(config.ui()) - .worldGuard(config.worldGuard()) - .playerSpawn(config.playerSpawn()) - .build(); - } - - private MineConfig withPlayerSpawn(MineConfig config, PlayerSpawnConfig spawn) { - return MineConfig.builder(config.name()) - .region(config.region()) - .blocks(config.blocks()) - .reset(config.reset()) - .rewards(config.rewards()) - .teleport(config.teleport()) - .ui(config.ui()) - .worldGuard(config.worldGuard()) - .playerSpawn(spawn) - .build(); - } - - public String formatLocation(Location loc) { + private String formatLocation(Location loc) { return String.format("%.1f, %.1f, %.1f in %s", loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName()); } diff --git a/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java b/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java deleted file mode 100644 index 2c82737..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java +++ /dev/null @@ -1,96 +0,0 @@ -package dev.loki.lomines.command.admin.info; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.block.BlockKey; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.util.location.geo.Cuboid; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.CommandSender; - -import java.util.List; -import java.util.Map; - -public class InfoCommand { - - private final LoMinesPlugin plugin; - - public InfoCommand(LoMinesPlugin plugin) { - this.plugin = plugin; - } - - public void handle(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm info ", NamedTextColor.RED)); - return; - } - String mineName = args[0]; - - Mine mine = plugin.getMines().find(mineName).orElse(null); - if (mine == null) { - sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - - var config = mine.getConfig(); - sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); - sender.sendMessage(Component.text("§6§lИнформация о шахте: §f" + mine.getName())); - sender.sendMessage(Component.text("")); - sender.sendMessage(Component.text("§7Мир: §f" + config.worldName())); - sender.sendMessage(Component.text("§7Блоков: §f" + mine.getBlocks() + "§7/§f" + mine.getTotalVolume() + - " §7(§f" + String.format("%.1f", mine.getPercentFilled()) + "%§7)")); - - printRegions(sender, mine.getRegions()); - printBlocks(sender, config.blocks().weights()); - printResetAndTeleport(sender, config); - - sender.sendMessage(Component.text("§6§lНаграды: §f" + config.rewards().entries().size() + " записей")); - sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); - } - - private void printRegions(CommandSender sender, List regions) { - sender.sendMessage(Component.text("§6§lРегионы:")); - for (int i = 0; i < regions.size(); i++) { - Cuboid r = regions.get(i); - sender.sendMessage(Component.text("§7 #" + (i + 1) + ": §f" + - r.getMinX() + "," + r.getMinY() + "," + r.getMinZ() + " §7-> §f" + - r.getMaxX() + "," + r.getMaxY() + "," + r.getMaxZ() + - " §7(§f" + r.getVolume() + " §7блоков)")); - } - sender.sendMessage(Component.text("")); - } - - private void printBlocks(CommandSender sender, Map weights) { - sender.sendMessage(Component.text("§6§lБлоки:")); - weights.entrySet().stream() - .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) - .limit(10) - .forEach(e -> sender.sendMessage(Component.text( - "§7 " + formatBlockKey(e.getKey()) + ": §f" + String.format("%.1f", e.getValue() * 100) + "%"))); - if (weights.size() > 10) { - sender.sendMessage(Component.text("§7 ... и ещё " + (weights.size() - 10) + " блоков")); - } - } - - private void printResetAndTeleport(CommandSender sender, MineConfig config) { - sender.sendMessage(Component.text("§6§lНастройки сброса:")); - sender.sendMessage(Component.text("§7 Интервал: §f" + config.reset().intervalDisplay())); - sender.sendMessage(Component.text("§7 Триггер по %: §f" + - (config.reset().isPercentTriggerEnabled() ? config.reset().percentTrigger() + "%" : "выкл"))); - - sender.sendMessage(Component.text("§6§lТелепортация:")); - sender.sendMessage(Component.text("§7 Точка телепорта: §f" + - (config.teleport().enabled() ? "§aустановлена" : "§7не задана"))); - sender.sendMessage(Component.text("§7 Точка спавна: §f" + - (config.playerSpawn().enabled() ? "§aустановлена" : "§7используется точка телепорта"))); - } - - private String formatBlockKey(BlockKey key) { - return switch (key) { - case BlockKey.Vanilla v -> v.material().name().toLowerCase(); - case BlockKey.Oraxen o -> "oraxen:" + o.id(); - case BlockKey.ItemsAdder i -> "ia:" + i.id(); - }; - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java b/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java deleted file mode 100644 index db92fe2..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java +++ /dev/null @@ -1,127 +0,0 @@ -package dev.loki.lomines.command.admin.info; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.data.config.region.RegionConfig; -import dev.loki.lomines.util.location.geo.Cuboid; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.util.ArrayList; -import java.util.List; - -public final class RegionActionHandler { - - private final LoMinesPlugin plugin; - - public RegionActionHandler(LoMinesPlugin plugin) { - this.plugin = plugin; - } - - public void addRegion(CommandSender sender, String mineName) { - if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED)); - return; - } - - Mine mine = plugin.getMines().find(mineName).orElse(null); - if (mine == null) { - player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - - Cuboid newRegion = resolveNewRegion(player, mine); - if (newRegion == null) { - return; - } - - List currentRegions = new ArrayList<>(mine.getRegions()); - currentRegions.add(newRegion); - - updateMineRegions(mine, new RegionConfig(currentRegions)); - - sendRegionAdded(player, newRegion, currentRegions.size()); - } - - private Cuboid resolveNewRegion(Player player, Mine mine) { - var wandManager = plugin.getGroupWandManager(); - var session = wandManager.getSession(player.getUniqueId()); - int slot = session.getActiveSlot(); - var pos1 = session.getPos1(slot); - var pos2 = session.getPos2(slot); - if (pos1 == null || pos2 == null) { - player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка")); - return null; - } - Cuboid newRegion = new Cuboid(pos1, pos2); - String mineWorld = mine.getConfig().region().worldName(); - String regionWorld = newRegion.getWorld().getName(); - - if (!mineWorld.equals(regionWorld)) { - player.sendMessage(Component.text("§cРегион должен быть в мире §f" + mineWorld + "§c, а не §f" + regionWorld)); - return null; - } - session.clearCorners(slot); - return newRegion; - } - - private void sendRegionAdded(Player player, Cuboid newRegion, int count) { - player.sendMessage(Component.text("§aДобавлен регион #" + count)); - player.sendMessage(Component.text("§7От: §f" + newRegion.getMinX() + ", " + newRegion.getMinY() + ", " + newRegion.getMinZ())); - player.sendMessage(Component.text("§7До: §f" + newRegion.getMaxX() + ", " + newRegion.getMaxY() + ", " + newRegion.getMaxZ())); - player.sendMessage(Component.text("§7Объём: §f" + newRegion.getVolume() + " §7блоков")); - } - - public void removeRegion(CommandSender sender, String mineName, int index) { - Mine mine = plugin.getMines().find(mineName).orElse(null); - if (mine == null) { - sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - - List currentRegions = new ArrayList<>(mine.getRegions()); - - if (currentRegions.size() <= 1) { - sender.sendMessage(Component.text("§cНельзя удалить последний регион! Удалите всю шахту.")); - return; - } - - if (index < 1 || index > currentRegions.size()) { - sender.sendMessage(Component.text("§cНеверный номер региона. Доступны: 1-" + currentRegions.size())); - return; - } - - Cuboid removed = currentRegions.remove(index - 1); - var newRegionConfig = new RegionConfig(currentRegions); - updateMineRegions(mine, newRegionConfig); - - sender.sendMessage(Component.text("§aУдалён регион #" + index)); - sender.sendMessage(Component.text("§7Был: §f" + removed.getMinX() + ", " + removed.getMinY() + ", " + removed.getMinZ() + - " §7-> §f" + removed.getMaxX() + ", " + removed.getMaxY() + ", " + removed.getMaxZ())); - sender.sendMessage(Component.text("§7Осталось регионов: §f" + currentRegions.size())); - } - - private void updateMineRegions(Mine mine, RegionConfig newConfig) { - var oldConfig = mine.getConfig(); - var newMineConfig = new MineConfig( - oldConfig.name(), - newConfig, - oldConfig.blocks(), - oldConfig.reset(), - oldConfig.rewards(), - oldConfig.teleport(), - oldConfig.ui(), - oldConfig.worldGuard(), - oldConfig.playerSpawn() - ); - - try { - plugin.getMines().updateMineConfig(mine.getName(), newMineConfig); - } catch (Exception e) { - throw new IllegalStateException("Failed to update mine regions", e); - } - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java b/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java deleted file mode 100644 index 95fca1f..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java +++ /dev/null @@ -1,79 +0,0 @@ -package dev.loki.lomines.command.admin.info; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.util.location.geo.Cuboid; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.CommandSender; - -import java.util.List; - -public class RegionCommands { - - private final LoMinesPlugin plugin; - private final RegionActionHandler actionHandler; - - public RegionCommands(LoMinesPlugin plugin) { - this.plugin = plugin; - this.actionHandler = new RegionActionHandler(plugin); - } - - public void handle(CommandSender sender, String subcommand, String[] args) { - switch (subcommand.toLowerCase()) { - case "regions" -> handleRegions(sender, args); - case "addregion" -> handleAddRegion(sender, args); - case "removeregion" -> handleRemoveRegion(sender, args); - default -> {} - } - } - - private void handleRegions(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm regions ", 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 regions = mine.getRegions(); - sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); - sender.sendMessage(Component.text("§6§lРегионы шахты §f" + mine.getName())); - sender.sendMessage(Component.text("§7Всего регионов: §f" + regions.size())); - sender.sendMessage(Component.text("")); - - for (int i = 0; i < regions.size(); i++) { - Cuboid r = regions.get(i); - sender.sendMessage(Component.text("§e#" + (i + 1) + " §7(§f" + r.getVolume() + " §7блоков)")); - sender.sendMessage(Component.text(" §7Мир: §f" + r.getWorld().getName())); - sender.sendMessage(Component.text(" §7От: §f" + r.getMinX() + ", " + r.getMinY() + ", " + r.getMinZ())); - sender.sendMessage(Component.text(" §7До: §f" + r.getMaxX() + ", " + r.getMaxY() + ", " + r.getMaxZ())); - } - sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); - } - - private void handleAddRegion(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm addregion ", NamedTextColor.RED)); - return; - } - actionHandler.addRegion(sender, args[0]); - } - - private void handleRemoveRegion(CommandSender sender, String[] args) { - if (args.length < 2) { - sender.sendMessage(Component.text("Usage: /lm removeregion ", 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)); - } - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/manage/AdminCommands.java b/src/main/java/dev/loki/lomines/command/admin/manage/AdminCommands.java deleted file mode 100644 index 1f51a8d..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/manage/AdminCommands.java +++ /dev/null @@ -1,90 +0,0 @@ -package dev.loki.lomines.command.admin.manage; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.core.mine.registry.Mines; -import dev.loki.lomines.util.ErrorHandler; -import net.kyori.adventure.text.Component; -import org.bukkit.command.CommandSender; - -import java.util.Collection; - -public class AdminCommands { - - private final LoMinesPlugin plugin; - private final Mines mines; - private final ErrorHandler errorHandler; - private final MineActionHandler actionHandler; - - public AdminCommands(LoMinesPlugin plugin) { - this.plugin = plugin; - this.mines = plugin.getMines(); - this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); - this.actionHandler = new MineActionHandler(plugin, mines, errorHandler); - } - - public void handle(CommandSender sender, String subcommand, String[] args) { - switch (subcommand.toLowerCase()) { - case "create" -> handleCreate(sender, args); - case "delete" -> handleDelete(sender, args); - case "edit" -> handleEdit(sender, args); - case "reset" -> handleReset(sender, args); - case "reload" -> actionHandler.reload(sender); - case "list" -> handleList(sender); - default -> {} - } - } - - private void handleCreate(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm create ")); - return; - } - actionHandler.create(sender, args[0]); - } - - private void handleDelete(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm delete ")); - return; - } - actionHandler.delete(sender, args[0]); - } - - private void handleEdit(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm edit ")); - return; - } - actionHandler.edit(sender, args[0]); - } - - private void handleReset(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm reset [silent]")); - return; - } - boolean silent = args.length > 1 && (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("silent")); - actionHandler.reset(sender, args[0], silent); - } - - private void handleList(CommandSender sender) { - Collection 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); - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java b/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java deleted file mode 100644 index 4608feb..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java +++ /dev/null @@ -1,92 +0,0 @@ -package dev.loki.lomines.command.admin.manage; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.model.MineConfig; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.CommandSender; - -public class CopyCommand { - - private final LoMinesPlugin plugin; - - public CopyCommand(LoMinesPlugin plugin) { - this.plugin = plugin; - } - - public void handle(CommandSender sender, String[] args) { - if (args.length < 2) { - sender.sendMessage(Component.text("Usage: /lm copy ", NamedTextColor.RED)); - return; - } - String fromMineName = args[0]; - String toMineName = args[1]; - - MinePair pair = resolveMines(sender, fromMineName, toMineName); - if (pair == null) { - return; - } - - var sourceConfig = pair.fromMine().getConfig(); - - MineConfig newTargetConfig = buildCopiedConfig(pair.toMine(), sourceConfig); - - try { - plugin.getMines().updateMineConfig(pair.toMine().getName(), newTargetConfig); - } catch (Exception e) { - sender.sendMessage(Component.text("Ошибка сохранения: " + e.getMessage(), NamedTextColor.RED)); - return; - } - - sendCopySummary(sender, pair.fromMine(), pair.toMine(), sourceConfig); - } - - private record MinePair(Mine fromMine, Mine toMine) { - } - - private MinePair resolveMines(CommandSender sender, String fromMineName, String toMineName) { - Mine fromMine = plugin.getMines().find(fromMineName).orElse(null); - if (fromMine == null) { - sender.sendMessage(Component.text("Исходная шахта не найдена: " + fromMineName, NamedTextColor.RED)); - return null; - } - - Mine toMine = plugin.getMines().find(toMineName).orElse(null); - if (toMine == null) { - sender.sendMessage(Component.text("Целевая шахта не найдена: " + toMineName, NamedTextColor.RED)); - return null; - } - - if (fromMine.getName().equalsIgnoreCase(toMine.getName())) { - sender.sendMessage(Component.text("Нельзя копировать шахту саму в себя!", NamedTextColor.RED)); - return null; - } - return new MinePair(fromMine, toMine); - } - - private MineConfig buildCopiedConfig(Mine toMine, MineConfig sourceConfig) { - var targetConfig = toMine.getConfig(); - return MineConfig.builder(toMine.getName()) - .region(targetConfig.region()) - .blocks(sourceConfig.blocks()) - .reset(sourceConfig.reset()) - .rewards(sourceConfig.rewards()) - .teleport(sourceConfig.teleport()) - .ui(sourceConfig.ui()) - .worldGuard(sourceConfig.worldGuard()) - .playerSpawn(sourceConfig.playerSpawn()) - .build(); - } - - private void sendCopySummary(CommandSender sender, Mine fromMine, Mine toMine, MineConfig sourceConfig) { - sender.sendMessage(Component.text("§aКонфигурация скопирована из §f" + fromMine.getName() + - " §aв §f" + toMine.getName())); - sender.sendMessage(Component.text("§7Скопировано:")); - sender.sendMessage(Component.text(" §7• Блоки: §f" + sourceConfig.blocks().weights().size() + " типов")); - sender.sendMessage(Component.text(" §7• Настройки сброса")); - sender.sendMessage(Component.text(" §7• Точки телепорта/спавна")); - sender.sendMessage(Component.text(" §7• Награды: §f" + sourceConfig.rewards().entries().size() + " записей")); - sender.sendMessage(Component.text("§7§oПримечание: регионы и позиции не скопированы")); - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/misc/TeleportCommands.java b/src/main/java/dev/loki/lomines/command/admin/misc/TeleportCommands.java deleted file mode 100644 index 77f7ada..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/misc/TeleportCommands.java +++ /dev/null @@ -1,30 +0,0 @@ -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 + " ", 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 -> {} - } - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java b/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java deleted file mode 100644 index f86c39b..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java +++ /dev/null @@ -1,121 +0,0 @@ -package dev.loki.lomines.command.admin.stats; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.core.mine.registry.Mines; -import dev.loki.lomines.data.stats.model.LeaderboardEntry; -import dev.loki.lomines.util.ErrorHandler; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.util.List; -import java.util.UUID; - -public final class LeaderboardRenderer { - - private LeaderboardRenderer() { - } - - public static void sendTopRows(CommandSender sender, List rows) { - if (rows.isEmpty()) { - sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY)); - return; - } - for (int i = 0; i < rows.size(); i++) { - LeaderboardEntry e = rows.get(i); - String name = formatPlayerName(e.playerId()); - sender.sendMessage(Component.text( - (i + 1) + ". " + name + " \u2014 " + e.count(), - NamedTextColor.WHITE)); - } - } - - public static String formatPlayerName(UUID id) { - String name = Bukkit.getOfflinePlayer(id).getName(); - return name != null ? name : id.toString(); - } - - public static int clampLimit(int limit) { - return Math.max(1, Math.min(50, limit)); - } - - public static Integer tryParsePositiveInt(String s) { - try { - int v = Integer.parseInt(s); - return v > 0 ? v : null; - } catch (NumberFormatException e) { - return null; - } - } - - public static UUID resolvePlayerUuid(CommandSender sender, String targetName, ErrorHandler errorHandler) { - String name = targetName == null ? "" : targetName.trim(); - if (name.isEmpty()) { - if (!(sender instanceof Player self)) { - errorHandler.sendError(sender, "Укажите игрока: /lm stats <ник>"); - return null; - } - return self.getUniqueId(); - } else { - if (!sender.hasPermission("lomines.stats.others")) { - errorHandler.handlePermissionDenied(sender, "смотреть чужую статистику"); - return null; - } - Player online = Bukkit.getPlayerExact(name); - if (online != null) { - return online.getUniqueId(); - } else { - OfflinePlayer off = Bukkit.getOfflinePlayer(name); - return off.getUniqueId(); - } - } - } - - public static void renderTopCommand(CommandSender sender, String arg1, String arg2, Mines mines, LoMinesPlugin plugin) { - TopRequest req = parseTopRequest(arg1, arg2); - if (req.mineName() != null) { - renderMineTop(sender, req.mineName(), req.limit(), mines, plugin); - return; - } - List rows = plugin.getStatsManager().getLeaderboard().getTopTotal(req.limit()); - sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD)); - sendTopRows(sender, rows); - } - - private record TopRequest(int limit, String mineName) { - } - - private static TopRequest parseTopRequest(String arg1, String arg2) { - String a = arg1 == null ? "" : arg1.trim(); - String b = arg2 == null ? "" : arg2.trim(); - if (a.isEmpty()) { - return new TopRequest(clampLimit(10), null); - } - if (b.isEmpty()) { - Integer asLimit = tryParsePositiveInt(a); - if (asLimit != null) { - return new TopRequest(clampLimit(asLimit), null); - } - return new TopRequest(clampLimit(10), a); - } - Integer limParsed = tryParsePositiveInt(b); - return new TopRequest(clampLimit(limParsed != null ? limParsed : 10), a); - } - - private static void renderMineTop(CommandSender sender, String mineName, int limit, Mines mines, LoMinesPlugin plugin) { - var found = mines.find(mineName); - if (found.isEmpty()) { - sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - Mine m = found.get(); - String key = m.getName(); - List rows = plugin.getStatsManager().getLeaderboard().getTopByMine(key, limit); - sender.sendMessage(Component.text("=== Топ по шахте \u00ab" + key + "\u00bb ===", NamedTextColor.GOLD)); - sendTopRows(sender, rows); - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java b/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java deleted file mode 100644 index 4cd0bb0..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java +++ /dev/null @@ -1,80 +0,0 @@ -package dev.loki.lomines.command.admin.stats; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.registry.Mines; -import dev.loki.lomines.data.stats.model.PlayerStats; -import dev.loki.lomines.data.stats.service.StatsManager; -import dev.loki.lomines.util.ErrorHandler; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.CommandSender; - -import java.util.Map; -import java.util.UUID; - -public class StatsCommands { - - private final LoMinesPlugin plugin; - private final Mines mines; - private final ErrorHandler errorHandler; - - public StatsCommands(LoMinesPlugin plugin) { - this.plugin = plugin; - this.mines = plugin.getMines(); - this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); - } - - public void handle(CommandSender sender, String subcommand, String[] args) { - switch (subcommand.toLowerCase()) { - case "stats" -> handleStats(sender, args); - case "top" -> { - if (isStatsDisabled(sender)) { - return; - } - String arg1 = args.length > 0 ? args[0] : ""; - String arg2 = args.length > 1 ? args[1] : ""; - LeaderboardRenderer.renderTopCommand(sender, arg1, arg2, mines, plugin); - } - default -> {} - } - } - - private void handleStats(CommandSender sender, String[] args) { - if (isStatsDisabled(sender)) { - return; - } - String targetName = args.length > 0 ? args[0] : ""; - UUID uuid = LeaderboardRenderer.resolvePlayerUuid(sender, targetName, errorHandler); - if (uuid == null) { - return; - } - - StatsManager sm = plugin.getStatsManager(); - PlayerStats ps = sm.getOrCreate(uuid); - String displayName = LeaderboardRenderer.formatPlayerName(uuid); - sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD)); - sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE)); - int pos = sm.getLeaderboard().getPosition(uuid); - if (pos > 0) { - sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY)); - } - Map byMine = ps.getMineStatsSnapshot(); - if (byMine.isEmpty()) { - return; - } - sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY)); - byMine.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .limit(12) - .forEach(e -> sender.sendMessage(Component.text( - " " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA))); - } - - private boolean isStatsDisabled(CommandSender sender) { - if (!plugin.getConfig().getBoolean("statistics-enabled", true)) { - sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED)); - return true; - } - return false; - } -} diff --git a/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java index 23400ee..0ac51f4 100644 --- a/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java +++ b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java @@ -4,26 +4,31 @@ import dev.loki.lomines.LoMinesPlugin; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; import java.util.ArrayList; 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 { + private final LoMinesPlugin plugin; + private static final List ALL_COMMANDS = List.of( "create", "delete", "reset", "reload", "list", "wand", "group", "stats", "top", "maskscan", "edit", "setteleport", "setspawn", - "clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help" + "clearspawn", "help" ); - private final LoMinesPlugin plugin; - private final SubcommandCompleter completer; - private final PermissionPredicate permission; + private static final List BOOLEAN_VALUES = List.of("true", "false"); + private static final List TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50"); public LoMinesTabCompleter(LoMinesPlugin plugin) { this.plugin = plugin; - this.completer = new SubcommandCompleter(plugin); - this.permission = new PermissionPredicate(); } @Override @@ -36,27 +41,130 @@ public class LoMinesTabCompleter implements TabCompleter { return switch (args.length) { case 1 -> completeSubcommands(sender, partial); - case 2 -> completer.completeSecondArg(sender, args[0].toLowerCase(), partial); - case 3 -> completer.completeThirdArg(args[0].toLowerCase(), args[1], partial); + case 2 -> completeSecondArg(sender, args[0].toLowerCase(), partial); + case 3 -> completeThirdArg(args[0].toLowerCase(), args[1], partial); default -> new ArrayList<>(); }; } private boolean isLoMinesCommand(String name) { - return "lm".equalsIgnoreCase(name) || - "lomines".equalsIgnoreCase(name) || - "mine".equalsIgnoreCase(name) || - "mines".equalsIgnoreCase(name); + return name.equalsIgnoreCase("lm") || + name.equalsIgnoreCase("lomines") || + name.equalsIgnoreCase("mine") || + name.equalsIgnoreCase("mines"); } private List completeSubcommands(CommandSender sender, String partial) { List completions = new ArrayList<>(); for (String cmd : ALL_COMMANDS) { - if (cmd.startsWith(partial) && permission.hasPermission(sender, cmd)) { + if (cmd.startsWith(partial) && hasPermission(sender, cmd)) { completions.add(cmd); } } return completions; } -} + private List completeSecondArg(CommandSender sender, String subcommand, String partial) { + return switch (subcommand) { + case "create" -> completeCreate(partial); + case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn" -> + filterStartsWith(getMineNames(), partial); + case "reset" -> completeReset(partial); + case "stats" -> completeStats(sender, partial); + case "top" -> completeTopFirstArg(partial); + default -> new ArrayList<>(); + }; + } + + private List completeThirdArg(String subcommand, String arg2, String partial) { + return switch (subcommand) { + case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial); + case "top" -> completeTopSecondArg(arg2, partial); + default -> new ArrayList<>(); + }; + } + + private List completeCreate(String partial) { + if (partial.isEmpty()) { + return new ArrayList<>(List.of("")); + } + return new ArrayList<>(); + } + + private List completeReset(String partial) { + List mines = filterStartsWith(getMineNames(), partial); + if (partial.isEmpty() || "true".startsWith(partial) || "false".startsWith(partial)) { + mines.addAll(filterStartsWith(BOOLEAN_VALUES, partial)); + } + return mines; + } + + private List completeStats(CommandSender sender, String partial) { + if (!sender.hasPermission("lomines.stats.others")) { + return new ArrayList<>(); + } + return filterStartsWith(getOnlinePlayerNames(), partial); + } + + private List completeTopFirstArg(String partial) { + List completions = new ArrayList<>(); + + completions.addAll(filterStartsWith(getMineNames(), partial)); + + if (isNumericPartial(partial)) { + completions.addAll(filterStartsWith(TOP_LIMITS, partial)); + } + + return completions; + } + + private List completeTopSecondArg(String firstArg, String partial) { + boolean firstArgWasMine = plugin.getMines().find(firstArg).isPresent(); + + if (firstArgWasMine && isNumericPartial(partial)) { + return filterStartsWith(TOP_LIMITS, partial); + } + + if (!firstArgWasMine && isNumericPartial(firstArg)) { + return filterStartsWith(getMineNames(), partial); + } + + return new ArrayList<>(); + } + + private boolean isNumericPartial(String partial) { + return partial.isEmpty() || partial.matches("\\d*"); + } + + private List filterStartsWith(List options, String partial) { + return options.stream() + .filter(s -> s.toLowerCase().startsWith(partial)) + .collect(Collectors.toList()); + } + + private List getMineNames() { + return plugin.getMines().getAll().stream() + .map(mine -> mine.getName()) + .collect(Collectors.toList()); + } + + private List getOnlinePlayerNames() { + return plugin.getServer().getOnlinePlayers().stream() + .map(Player::getName) + .collect(Collectors.toList()); + } + + private boolean hasPermission(CommandSender sender, String cmd) { + return switch (cmd) { + case "create", "delete", "reload", "list", "maskscan", "edit" -> + sender.hasPermission("lomines.admin"); + case "reset" -> sender.hasPermission("lomines.admin.reset"); + case "setteleport" -> sender.hasPermission("lomines.admin.setteleport"); + case "setspawn", "clearspawn" -> sender.hasPermission("lomines.admin.setspawn"); + case "wand", "group" -> sender.hasPermission("lomines.admin.wand"); + case "stats", "top" -> sender.hasPermission("lomines.stats"); + case "help" -> sender.hasPermission("lomines.use"); + default -> true; + }; + } +} diff --git a/src/main/java/dev/loki/lomines/command/common/PermissionPredicate.java b/src/main/java/dev/loki/lomines/command/common/PermissionPredicate.java deleted file mode 100644 index 14c2ced..0000000 --- a/src/main/java/dev/loki/lomines/command/common/PermissionPredicate.java +++ /dev/null @@ -1,23 +0,0 @@ -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; - }; - } -} diff --git a/src/main/java/dev/loki/lomines/command/common/SubcommandCompleter.java b/src/main/java/dev/loki/lomines/command/common/SubcommandCompleter.java deleted file mode 100644 index 2bc1f26..0000000 --- a/src/main/java/dev/loki/lomines/command/common/SubcommandCompleter.java +++ /dev/null @@ -1,130 +0,0 @@ -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 BOOLEAN_VALUES = List.of("true", "false"); - private static final List TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50"); - - private final LoMinesPlugin plugin; - - public SubcommandCompleter(LoMinesPlugin plugin) { - this.plugin = plugin; - } - - public List 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 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 completeRegionIndex(String mineName, String partial) { - var mine = plugin.getMines().find(mineName); - if (mine.isEmpty()) { - return new ArrayList<>(); - } - int regionCount = mine.get().getRegions().size(); - List indices = new ArrayList<>(); - for (int i = 1; i <= regionCount; i++) { - indices.add(String.valueOf(i)); - } - return filterStartsWith(indices, partial); - } - - private List completeCreate(String partial) { - if (partial.isEmpty()) { - return new ArrayList<>(List.of("")); - } - return new ArrayList<>(); - } - - private List completeReset(String partial) { - List mines = filterStartsWith(getMineNames(), partial); - if (partial.isEmpty() || "true".startsWith(partial) || "false".startsWith(partial)) { - mines.addAll(filterStartsWith(BOOLEAN_VALUES, partial)); - } - return mines; - } - - private List completeStats(CommandSender sender, String partial) { - if (!sender.hasPermission("lomines.stats.others")) { - return new ArrayList<>(); - } - return filterStartsWith(getOnlinePlayerNames(), partial); - } - - private List completeTopFirstArg(String partial) { - List completions = new ArrayList<>(); - completions.addAll(filterStartsWith(getMineNames(), partial)); - if (isNumericPartial(partial)) { - completions.addAll(filterStartsWith(TOP_LIMITS, partial)); - } - return completions; - } - - private List 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 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 filterStartsWith(List options, String partial) { - return options.stream() - .filter(s -> s.toLowerCase().startsWith(partial)) - .collect(Collectors.toList()); - } - - public List getMineNames() { - return plugin.getMines().getAll().stream() - .map(mine -> mine.getName()) - .collect(Collectors.toList()); - } - - private List getOnlinePlayerNames() { - return plugin.getServer().getOnlinePlayers().stream() - .map(Player::getName) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java b/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java index ea1bd63..ccf9178 100644 --- a/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java +++ b/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java @@ -3,11 +3,19 @@ package dev.loki.lomines.command.player; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.util.ErrorHandler; 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.format.NamedTextColor; import org.bukkit.command.CommandSender; 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 { private final LoMinesPlugin plugin; @@ -18,32 +26,26 @@ public class PlayerCommands { this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); } - public void handle(CommandSender sender, String subcommand, String[] args) { - switch (subcommand.toLowerCase()) { - case "wand" -> { - if (!(sender instanceof Player player)) { - errorHandler.sendError(sender, "Только для игрока."); - return; - } - player.getInventory().addItem(GroupWandItem.create(plugin)); - player.sendMessage(Component.text( - "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", - NamedTextColor.GREEN)); - } - case "group" -> { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm group ", NamedTextColor.RED)); - return; - } - if (!(sender instanceof Player player)) { - errorHandler.sendError(sender, "Префикс задаётся в игре."); - return; - } - plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(args[0]); - String p = args[0].trim(); - player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA)); - } - default -> {} + @Subcommand(value = "wand", permission = "lomines.admin.wand") + public void wand(CommandSender sender) { + if (!(sender instanceof Player player)) { + errorHandler.sendError(sender, "Только для игрока."); + return; } + player.getInventory().addItem(GroupWandItem.create(plugin)); + player.sendMessage(Component.text( + "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", + NamedTextColor.GREEN)); + } + + @Subcommand(value = "group", permission = "lomines.admin.wand") + public void group(CommandSender sender, @Arg("prefix") String prefix) { + if (!(sender instanceof Player player)) { + errorHandler.sendError(sender, "Префикс задаётся в игре."); + return; + } + plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix); + String p = prefix.trim(); + player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA)); } } diff --git a/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java b/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java deleted file mode 100644 index 75d0d8d..0000000 --- a/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java +++ /dev/null @@ -1,63 +0,0 @@ -package dev.loki.lomines.command.player; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.util.location.geo.Cuboid; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Location; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -public class TeleportCommand { - - private final LoMinesPlugin plugin; - - public TeleportCommand(LoMinesPlugin plugin) { - this.plugin = plugin; - } - - public void handle(CommandSender sender, String[] args) { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm tp ", NamedTextColor.RED)); - return; - } - String mineName = args[0]; - - if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED)); - return; - } - - Mine mine = plugin.getMines().find(mineName).orElse(null); - if (mine == null) { - player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - - Location target = resolveTarget(player, mine); - if (target == null) { - return; - } - - player.teleport(target); - player.sendMessage(Component.text("§aТелепортировано к шахте §f" + mine.getName() + - " §a(§f" + String.format("%.0f, %.0f, %.0f", target.getX(), target.getY(), target.getZ()) + "§a)")); - } - - private Location resolveTarget(Player player, Mine mine) { - var config = mine.getConfig(); - if (config.teleport().enabled() && config.teleport().getLocation().isPresent()) { - return config.teleport().getLocation().get(); - } - if (!mine.getRegions().isEmpty()) { - Cuboid region = mine.getRegions().get(0); - double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5; - double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5; - double centerY = region.getMaxY() + 1; - return new Location(region.getWorld(), centerX, centerY, centerZ); - } - player.sendMessage(Component.text("У шахты нет настроенной точки телепорта!", NamedTextColor.RED)); - return null; - } -} diff --git a/src/main/java/dev/loki/lomines/core/common/package-info.java b/src/main/java/dev/loki/lomines/core/common/package-info.java new file mode 100644 index 0000000..2228c11 --- /dev/null +++ b/src/main/java/dev/loki/lomines/core/common/package-info.java @@ -0,0 +1,11 @@ +/** + * Core components of the LoMines plugin. + *

+ * This package contains the main plugin class and core system classes: + *

    + *
  • {@code LoMinesPlugin} - Main plugin class extending LoPlugin
  • + *
  • {@code Mines} - Registry for managing all active mines
  • + *
  • {@code Mine} - Representation of a single mine with its configuration and state
  • + *
+ */ +package dev.loki.lomines.core.common; diff --git a/src/main/java/dev/loki/lomines/core/mine/model/Mine.java b/src/main/java/dev/loki/lomines/core/mine/Mine.java similarity index 59% rename from src/main/java/dev/loki/lomines/core/mine/model/Mine.java rename to src/main/java/dev/loki/lomines/core/mine/Mine.java index ad4aea0..800faaa 100644 --- a/src/main/java/dev/loki/lomines/core/mine/model/Mine.java +++ b/src/main/java/dev/loki/lomines/core/mine/Mine.java @@ -1,28 +1,31 @@ -package dev.loki.lomines.core.mine.model; +package dev.loki.lomines.core.mine; import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.block.BlockSetter; -import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.FillMode; -import dev.loki.lomines.handler.ui.ActionBarHandler; -import dev.loki.lomines.handler.block.MineBlockHandler; -import dev.loki.lomines.handler.reset.MineResetHandler; +import dev.loki.lomines.handler.ActionBarHandler; +import dev.loki.lomines.handler.MineBlockHandler; +import dev.loki.lomines.handler.MineResetHandler; import dev.loki.lomines.util.location.BlockKeys; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.LocationParser; -import org.bukkit.Bukkit; -import org.bukkit.scheduler.BukkitTask; +import dev.lolib.scheduler.ScheduledTask; +import dev.lolib.scheduler.Scheduler; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Player; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.util.*; 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 { private final String name; @@ -30,12 +33,14 @@ public final class Mine { private final MineConfig config; private final BlockSetter blockSetter; private final LoMinesPlugin plugin; - private final MineState state; + private final int totalVolume; + private final Set maskBlockKeys; + private final AtomicInteger blocks; + private final AtomicInteger ticks; private final MineResetHandler resetHandler; private final MineBlockHandler blockHandler; private final ActionBarHandler actionBarHandler; - private BukkitTask actionBarTask; - private final Set maskBlockKeys; + private ScheduledTask actionBarTask; public Mine(String name, MineConfig config, LoMinesPlugin plugin) { this.name = name; @@ -44,21 +49,30 @@ public final class Mine { this.regions = config.region().regions(); this.blockSetter = createBlockSetter(config, plugin); this.maskBlockKeys = initMaskBlockKeys(config); - this.state = new MineState(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); this.resetHandler = new MineResetHandler(this, plugin); this.blockHandler = new MineBlockHandler(this, plugin); this.actionBarHandler = new ActionBarHandler(this); } private Set initMaskBlockKeys(MineConfig config) { - if (config.blocks().fillMode() != FillMode.MASK || config.blocks().mask() == null) { + if (config.blocks().fillMode() != FillMode.MASK) { return Set.of(); } - return config.blocks().mask().positions().keySet().stream() - .map(LocationParser::parseSimple) - .filter(Objects::nonNull) - .map(BlockKeys::key) - .collect(Collectors.toSet()); + Set 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 void start() { @@ -85,15 +99,18 @@ public final class Mine { } public boolean appliesToBlock(Location location) { - return contains(location) && (config.blocks().fillMode() != FillMode.MASK || maskBlockKeys.contains(BlockKeys.key(location))); + if (!contains(location)) return false; + if (config.blocks().fillMode() != FillMode.MASK) return true; + return maskBlockKeys.contains(BlockKeys.key(location)); } public int getTotalVolume() { - return state.getTotalVolume(); + return totalVolume; } public double getPercentFilled() { - return state.getPercentFilled(); + if (totalVolume == 0) return 0.0; + return (double) blocks.get() / totalVolume * 100.0; } private BlockSetter createBlockSetter(MineConfig config, LoMinesPlugin plugin) { @@ -103,16 +120,17 @@ public final class Mine { } BlockKey firstKey = weights.keySet().iterator().next(); return switch (firstKey) { - case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("External plugins disabled"); - case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("External plugins disabled"); + case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("Oraxen integration is currently disabled"); + case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("ItemsAdder integration is currently disabled"); case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin); }; } private void startActionBarTask() { - if (config.ui().actionBarEnabled()) { - actionBarTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> actionBarHandler.sendToNearbyPlayers(), 10L, 10L); - } + if (!config.ui().actionBarEnabled()) return; + actionBarTask = Scheduler.get(plugin).runTimer(() -> { + actionBarHandler.sendToNearbyPlayers(); + }, 10L, 10L); } public String getName() { @@ -131,19 +149,19 @@ public final class Mine { return regions; } + public AtomicInteger getBlocksAtomic() { + return blocks; + } + public int getBlocks() { - return state.getBlocks(); + return blocks.get(); } public int getTicks() { - return state.getTicks(); - } - - public AtomicInteger getBlocksAtomic() { - return state.getBlocksAtomic(); + return ticks.get(); } public AtomicInteger getTicksAtomic() { - return state.getTicksAtomic(); + return ticks; } } diff --git a/src/main/java/dev/loki/lomines/core/mine/service/MineLoader.java b/src/main/java/dev/loki/lomines/core/mine/MineLoader.java similarity index 70% rename from src/main/java/dev/loki/lomines/core/mine/service/MineLoader.java rename to src/main/java/dev/loki/lomines/core/mine/MineLoader.java index f69f6e1..2646503 100644 --- a/src/main/java/dev/loki/lomines/core/mine/service/MineLoader.java +++ b/src/main/java/dev/loki/lomines/core/mine/MineLoader.java @@ -1,10 +1,10 @@ -package dev.loki.lomines.core.mine.service; +package dev.loki.lomines.core.common; import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.service.MineFileManager; -import dev.loki.lomines.core.service.MineRepository; -import dev.loki.lomines.data.config.ConfigLoader; -import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; +import dev.loki.lomines.data.config.MineConfig; import java.io.IOException; import java.nio.file.Files; @@ -14,19 +14,19 @@ import java.util.stream.Stream; /** * Loads mine configurations from disk. */ -public final class MineLoader { +final class MineLoader { private final LoMinesPlugin plugin; private final MineFileManager fileManager; private final MineRepository repository; - public MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) { + MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) { this.plugin = plugin; this.fileManager = fileManager; this.repository = repository; } - public void loadAll() throws IOException { + void loadAll() throws IOException { repository.clear(); Path minesFolder = fileManager.getMinesFolder(); @@ -49,9 +49,9 @@ public final class MineLoader { MineConfig config = fileManager.loadConfig(mineName); repository.createAndStart(mineName, config); - plugin.getLogger().info("Loaded mine: " + mineName); + plugin.loLogger().info("Loaded mine: " + mineName); } catch (IOException | ConfigLoader.ConfigLoadException e) { - plugin.getLogger().severe("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage()); + plugin.loLogger().error("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage()); } } } diff --git a/src/main/java/dev/loki/lomines/core/mine/service/MineTicker.java b/src/main/java/dev/loki/lomines/core/mine/MineTicker.java similarity index 76% rename from src/main/java/dev/loki/lomines/core/mine/service/MineTicker.java rename to src/main/java/dev/loki/lomines/core/mine/MineTicker.java index bb10629..59dc3b5 100644 --- a/src/main/java/dev/loki/lomines/core/mine/service/MineTicker.java +++ b/src/main/java/dev/loki/lomines/core/mine/MineTicker.java @@ -1,10 +1,8 @@ -package dev.loki.lomines.core.mine.service; +package dev.loki.lomines.core.mine; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.core.mine.registry.Mines; -import org.bukkit.Bukkit; -import org.bukkit.scheduler.BukkitTask; +import dev.lolib.scheduler.ScheduledTask; +import dev.lolib.scheduler.Scheduler; /** * Centralized ticker for all mines. @@ -15,7 +13,7 @@ public final class MineTicker { private final Mines mines; private final LoMinesPlugin plugin; - private BukkitTask tickerTask; + private ScheduledTask tickerTask; public MineTicker(Mines mines, LoMinesPlugin plugin) { this.mines = mines; @@ -31,7 +29,7 @@ public final class MineTicker { return; // Already running } - tickerTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + tickerTask = Scheduler.get(plugin).runTimer(() -> { for (Mine mine : mines.getAll()) { tickMine(mine); } @@ -54,7 +52,7 @@ public final class MineTicker { private void tickMine(Mine mine) { int currentTicks = mine.getTicksAtomic().incrementAndGet(); - if (currentTicks >= mine.getConfig().reset().intervalTicks()) { + if (currentTicks >= mine.getConfig().getResetTicks()) { mine.getTicksAtomic().set(0); mine.reset(false); } diff --git a/src/main/java/dev/loki/lomines/core/mine/registry/Mines.java b/src/main/java/dev/loki/lomines/core/mine/Mines.java similarity index 63% rename from src/main/java/dev/loki/lomines/core/mine/registry/Mines.java rename to src/main/java/dev/loki/lomines/core/mine/Mines.java index 5b55f1c..6c08932 100644 --- a/src/main/java/dev/loki/lomines/core/mine/registry/Mines.java +++ b/src/main/java/dev/loki/lomines/core/mine/Mines.java @@ -1,20 +1,23 @@ -package dev.loki.lomines.core.mine.registry; +package dev.loki.lomines.core.mine; import dev.loki.lomines.LoMinesPlugin; -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.common.MineLoader; +import dev.loki.lomines.core.service.MaskScanService; import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.core.service.MineRepository; -import dev.loki.lomines.data.config.ConfigLoader; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.integration.worldguard.region.WorldGuardRegionService; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; +import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.integration.worldguard.WorldGuardRegionService; import org.bukkit.Location; import java.io.IOException; import java.util.Collection; import java.util.Optional; +/** + * Registry for all active mines. + * Manages the lifecycle of mines including loading, creating, and deleting. + */ public final class Mines { private final LoMinesPlugin plugin; @@ -23,7 +26,6 @@ public final class Mines { private final MineLoader loader; private final MaskScanService maskScanService; private final WorldGuardRegionService worldGuardService; - private final MineFinder finder; public Mines(LoMinesPlugin plugin) { this.plugin = plugin; @@ -32,7 +34,6 @@ public final class Mines { this.loader = new MineLoader(plugin, fileManager, repository); this.maskScanService = new MaskScanService(repository, fileManager); this.worldGuardService = new WorldGuardRegionService(plugin); - this.finder = new MineFinder(repository); } public void loadAll() throws IOException { @@ -44,7 +45,6 @@ public final class Mines { } public void create(String name, Location corner1, Location corner2) throws IOException { - name = name.toLowerCase().trim(); if (repository.exists(name)) { throw new IllegalArgumentException("Mine already exists: " + name); } @@ -64,7 +64,7 @@ public final class Mines { worldGuardService.createRegion(name, config); repository.createAndStart(name, config); - plugin.getLogger().info("Created mine: " + name); + plugin.loLogger().info("Created mine: " + name); } public void delete(String name) throws IOException { @@ -79,65 +79,39 @@ public final class Mines { } public Mine get(String name) { - return finder.get(name); + return repository.get(name); } public Optional find(String name) { - return finder.find(name); + return repository.find(name); } public Collection getAll() { - return finder.getAll(); + return repository.getAll(); } public Optional findByLocation(Location location) { - return finder.findByLocation(location); + return repository.findByLocation(location); } public int scanAndSaveMask(String mineName) throws IOException, ConfigLoader.ConfigLoadException { int count = maskScanService.scanAndSave(mineName); - plugin.getLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)"); + plugin.loLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)"); return count; } public void reloadMine(String name) throws IOException, ConfigLoader.ConfigLoadException { - Mine oldMine = finder.find(name).orElse(null); + Mine oldMine = repository.find(name).orElse(null); repository.reload(name); if (oldMine != null) { - Mine newMine = finder.find(name).orElse(null); + Mine newMine = repository.find(name).orElse(null); if (newMine != null) { worldGuardService.updateRegion(name, newMine.getConfig()); } } } - public void updateMineConfig(String name, MineConfig newConfig) throws IOException { - if (!repository.exists(name)) { - throw new IllegalArgumentException("Mine not found: " + name); - } - - try { - fileManager.saveConfig(name, newConfig); - } catch (ConfigLoader.ConfigLoadException e) { - throw new IOException("Failed to save mine config: " + e.getMessage(), e); - } - - try { - Mine oldMine = finder.find(name).orElse(null); - repository.reload(name); - - if (oldMine != null) { - Mine newMine = finder.find(name).orElse(null); - if (newMine != null) { - worldGuardService.updateRegion(name, newMine.getConfig()); - } - } - } catch (ConfigLoader.ConfigLoadException e) { - throw new IOException("Failed to reload mine after config update: " + e.getMessage(), e); - } - } - public WorldGuardRegionService getWorldGuardService() { return worldGuardService; } diff --git a/src/main/java/dev/loki/lomines/core/mine/model/MineState.java b/src/main/java/dev/loki/lomines/core/mine/model/MineState.java deleted file mode 100644 index 99b5e3f..0000000 --- a/src/main/java/dev/loki/lomines/core/mine/model/MineState.java +++ /dev/null @@ -1,82 +0,0 @@ -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 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 initMaskBlockKeys(MineConfig config) { - if (config.blocks().fillMode() != FillMode.MASK) { - return Set.of(); - } - Set 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 getMaskBlockKeys() { - return maskBlockKeys; - } - - public boolean appliesToBlock(Location location) { - if (maskBlockKeys.isEmpty()) { - return true; - } - return maskBlockKeys.contains(BlockKeys.key(location)); - } -} diff --git a/src/main/java/dev/loki/lomines/core/mine/registry/MineFinder.java b/src/main/java/dev/loki/lomines/core/mine/registry/MineFinder.java deleted file mode 100644 index ffef0ef..0000000 --- a/src/main/java/dev/loki/lomines/core/mine/registry/MineFinder.java +++ /dev/null @@ -1,33 +0,0 @@ -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 find(String name) { - return repository.find(name); - } - - public Collection getAll() { - return repository.getAll(); - } - - public Optional findByLocation(Location location) { - return repository.findByLocation(location); - } -} diff --git a/src/main/java/dev/loki/lomines/core/service/mask/MaskScanService.java b/src/main/java/dev/loki/lomines/core/service/MaskScanService.java similarity index 83% rename from src/main/java/dev/loki/lomines/core/service/mask/MaskScanService.java rename to src/main/java/dev/loki/lomines/core/service/MaskScanService.java index bbce989..9c4e7a5 100644 --- a/src/main/java/dev/loki/lomines/core/service/mask/MaskScanService.java +++ b/src/main/java/dev/loki/lomines/core/service/MaskScanService.java @@ -1,12 +1,10 @@ -package dev.loki.lomines.core.service.mask; +package dev.loki.lomines.core.service; -import dev.loki.lomines.core.mine.model.Mine; -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.core.mine.Mine; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.util.ValidationUtils; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.selection.MaskScanner; import org.bukkit.Location; import org.bukkit.Material; @@ -59,8 +57,8 @@ public final class MaskScanService { } private Material parseMarkerMaterial(BlockKey key) { - if (key instanceof BlockKey.Vanilla(Material material)) { - return material; + if (key instanceof BlockKey.Vanilla vanilla) { + return vanilla.material(); } // For custom blocks (Oraxen/ItemsAdder), we need to get the underlying material // This would need integration with those plugins to get the actual material diff --git a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java index bfe1fbc..3b77c63 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java +++ b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java @@ -1,25 +1,38 @@ 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.model.MineConfig; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; +import dev.loki.lomines.data.config.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.WorldGuardConfig; +import dev.loki.lomines.util.location.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 MineFileManager { - - private final Path minesFolder; - private final ConfigLoader configLoader; - private final MineConfigWriter configWriter; +/** + * Handles file operations for mine configurations. + * Updated for new section-based configuration (v2). + */ +public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { public MineFileManager(Path minesFolder) { - this.minesFolder = minesFolder; - this.configLoader = new ConfigLoader(minesFolder.getParent()); - this.configWriter = new MineConfigWriter(minesFolder, configLoader); + this(minesFolder, new ConfigLoader(minesFolder.getParent())); } public Path getMinesFolder() { @@ -32,9 +45,82 @@ public final class MineFileManager { } } + /** + * 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 { 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 weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 1.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + // Load WorldGuard defaults from _defaults.yml + WorldGuardConfig wgConfig = loadWorldGuardDefaults(); + + // Build mine config with defaults + MineConfig config = MineConfig.builder(name) + .region(region) + .blocks(blocks) + .reset(ResetConfig.defaults()) + .rewards(RewardConfig.empty()) + .teleport(TeleportConfig.disabled()) + .ui(UIConfig.defaults()) + .worldGuard(wgConfig) + .playerSpawn(PlayerSpawnConfig.disabled()) + .build(); + + // Save using new loader + configLoader.save(config); + } + + /** + * Loads WorldGuard configuration from defaults.yml. + */ + private WorldGuardConfig loadWorldGuardDefaults() { + Path defaultsPath = minesFolder.resolve("_defaults.yml"); + if (!Files.exists(defaultsPath)) { + return WorldGuardConfig.disabled(); + } + + try { + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(defaultsPath.toFile()); + ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard"); + + if (wgSection == null) { + return WorldGuardConfig.disabled(); + } + + boolean enabled = wgSection.getBoolean("enabled", false); + if (!enabled) { + return WorldGuardConfig.disabled(); + } + + String template = wgSection.getString("region-template", "{mine_name}_{random_4}"); + + return WorldGuardConfig.builder() + .enabled(true) + .template(template) + .owners(wgSection.getStringList("owners")) + .members(wgSection.getStringList("members")) + .flags(wgSection.getStringList("flags")) + .protectOnCreate(wgSection.getBoolean("protect-on-create", true)) + .build(); + + } catch (Exception e) { + return WorldGuardConfig.disabled(); + } } public MineConfig loadConfig(String name) throws IOException, ConfigLoader.ConfigLoadException { @@ -48,16 +134,48 @@ public final class MineFileManager { } } + /** + * 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 { configLoader.save(config); } - public void saveConfig(String name, MineConfig config) throws ConfigLoader.ConfigLoadException { - configLoader.save(config); - } + /** + * Saves mask positions for mask fill mode. + */ + public void saveMaskPositions(String name, BlockKey markerMaterial, java.util.List positions) throws IOException, ConfigLoader.ConfigLoadException { + MineConfig config = loadConfig(name); - public void saveMaskPositions(String name, BlockKey markerMaterial, - java.util.List positions) throws IOException, ConfigLoader.ConfigLoadException { - configWriter.saveMaskPositions(name, markerMaterial, positions); + // Build new mask config + Map 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); } } diff --git a/src/main/java/dev/loki/lomines/core/service/MineRepository.java b/src/main/java/dev/loki/lomines/core/service/MineRepository.java index 862377f..28c8224 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineRepository.java +++ b/src/main/java/dev/loki/lomines/core/service/MineRepository.java @@ -1,17 +1,13 @@ package dev.loki.lomines.core.service; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.ConfigLoader; -import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.loader.common.ConfigLoader; +import dev.loki.lomines.data.config.MineConfig; import org.bukkit.Location; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -88,6 +84,6 @@ public final class MineRepository { stopAndRemove(name); MineConfig config = fileManager.loadConfig(name); Mine mine = createAndStart(name, config); - plugin.getLogger().info("Reloaded mine: " + name); + plugin.loLogger().info("Reloaded mine: " + name); } } diff --git a/src/main/java/dev/loki/lomines/core/service/write/MineConfigWriter.java b/src/main/java/dev/loki/lomines/core/service/write/MineConfigWriter.java deleted file mode 100644 index a683563..0000000 --- a/src/main/java/dev/loki/lomines/core/service/write/MineConfigWriter.java +++ /dev/null @@ -1,129 +0,0 @@ -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 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 positions) throws IOException, ConfigLoader.ConfigLoadException { - MineConfig config = configLoader.load(name); - - Map 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(); - } - } -} diff --git a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java index fb41bae..c0e32ff 100644 --- a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java @@ -5,21 +5,27 @@ import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader; import dev.loki.lomines.data.config.loader.entity.TeleportConfigLoader; import dev.loki.lomines.data.config.loader.region.RegionConfigLoader; import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader; +import dev.loki.lomines.data.config.loader.reset.ResetConfigLoader; import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader; -import dev.loki.lomines.data.config.loader.system.ResetConfigLoader; import dev.loki.lomines.data.config.loader.system.UIConfigLoader; -import dev.loki.lomines.data.config.model.MineConfig; import org.bukkit.configuration.file.YamlConfiguration; import java.io.IOException; import java.nio.file.Files; 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 { private final Path dataFolder; - private final DefaultsMerger defaultsMerger; + private YamlConfiguration defaults; + // Section loaders private RegionConfigLoader regionLoader; private BlockConfigLoader blockLoader; private ResetConfigLoader resetLoader; @@ -31,29 +37,69 @@ public final class ConfigLoader { public ConfigLoader(Path dataFolder) { this.dataFolder = dataFolder; - this.defaultsMerger = new DefaultsMerger(dataFolder); + loadDefaults(); 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", "{mine} {percent}% ({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() { regionLoader = new RegionConfigLoader(); blockLoader = new BlockConfigLoader(); - resetLoader = new ResetConfigLoader(defaultsMerger.getDefaults()); + resetLoader = new ResetConfigLoader(defaults); rewardLoader = new RewardConfigLoader(); teleportLoader = new TeleportConfigLoader(); - uiLoader = new UIConfigLoader(defaultsMerger.getDefaults()); - worldGuardLoader = new WorldGuardConfigLoader(defaultsMerger.getDefaults()); + uiLoader = new UIConfigLoader(defaults); + worldGuardLoader = new WorldGuardConfigLoader(defaults); playerSpawnLoader = new PlayerSpawnConfigLoader(); } + /** + * Load a mine configuration from file. + */ public MineConfig load(String mineName) throws ConfigLoadException { - if (!Files.exists(dataFolder.resolve("mines").resolve(mineName + ".yml"))) { + Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml"); + + if (!Files.exists(configPath)) { throw new ConfigLoadException("Mine not found: " + mineName); } - Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml"); YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configPath.toFile()); - defaultsMerger.mergeDefaults(yaml); + mergeDefaults(yaml); try { return MineConfig.builder(mineName) @@ -71,7 +117,11 @@ public final class ConfigLoader { } } + /** + * Save a mine configuration to file. + */ public void save(MineConfig config) throws ConfigLoadException { + Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml"); YamlConfiguration yaml = new YamlConfiguration(); regionLoader.save(yaml, config.region()); @@ -84,7 +134,6 @@ public final class ConfigLoader { playerSpawnLoader.save(yaml, config.playerSpawn()); try { - Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml"); Files.createDirectories(configPath.getParent()); yaml.save(configPath.toFile()); } catch (IOException e) { @@ -92,11 +141,21 @@ 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 ConfigLoadException(String message) { super(message); } - public ConfigLoadException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/dev/loki/lomines/data/config/DefaultsMerger.java b/src/main/java/dev/loki/lomines/data/config/DefaultsMerger.java deleted file mode 100644 index a454c19..0000000 --- a/src/main/java/dev/loki/lomines/data/config/DefaultsMerger.java +++ /dev/null @@ -1,71 +0,0 @@ -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", "{mine} {percent}% ({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)); - } - } - } -} diff --git a/src/main/java/dev/loki/lomines/data/config/model/MineConfigDefaults.java b/src/main/java/dev/loki/lomines/data/config/MineConfig.java similarity index 52% rename from src/main/java/dev/loki/lomines/data/config/model/MineConfigDefaults.java rename to src/main/java/dev/loki/lomines/data/config/MineConfig.java index 593c5fa..2a1c8bb 100644 --- a/src/main/java/dev/loki/lomines/data/config/model/MineConfigDefaults.java +++ b/src/main/java/dev/loki/lomines/data/config/MineConfig.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config.model; +package dev.loki.lomines.data.config; import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.region.RegionConfig; @@ -7,13 +7,72 @@ 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.integration.worldguard.WorldGuardConfig; -public final class MineConfigDefaults { +import org.bukkit.Location; - private MineConfigDefaults() { +import java.util.Objects; +import java.util.Optional; + +/** + * Clean, type-safe, section-based mine configuration. + * Proper separation of concerns: each section handles one aspect. + */ +public record MineConfig( + String name, + RegionConfig region, + BlockConfig blocks, + ResetConfig reset, + RewardConfig rewards, + TeleportConfig teleport, + UIConfig ui, + WorldGuardConfig worldGuard, + PlayerSpawnConfig playerSpawn +) { + + public MineConfig { + Objects.requireNonNull(name, "Mine name cannot be null"); + if (name.isBlank()) { + throw new IllegalArgumentException("Mine name cannot be blank"); + } + name = name.toLowerCase().trim(); + + Objects.requireNonNull(region, "Region config cannot be null"); + Objects.requireNonNull(blocks, "Block config cannot be null"); + Objects.requireNonNull(reset, "Reset config cannot be null"); + Objects.requireNonNull(rewards, "Reward config cannot be null"); + Objects.requireNonNull(teleport, "Teleport config cannot be null"); + Objects.requireNonNull(ui, "UI config cannot be null"); + // worldGuard and playerSpawn can be null (disabled by default) + if (playerSpawn == null) { + playerSpawn = PlayerSpawnConfig.disabled(); + } } + /** + * Total volume of the mine (for progress calculation). + */ + public int volume() { + return region.totalVolume(); + } + + /** + * World name where the mine is located. + */ + public String worldName() { + return region.worldName(); + } + + /** + * Creates builder for fluent construction. + */ + public static Builder builder(String name) { + return new Builder(name); + } + + /** + * Creates config with sensible defaults. + */ public static MineConfig defaults(String name, RegionConfig region, BlockConfig blocks) { return new MineConfig( name, @@ -28,6 +87,18 @@ public final class MineConfigDefaults { ); } + /** + * Returns the spawn location for stuck players. + * If playerSpawn is not set, falls back to teleport location. + */ + public Optional getSpawnForStuckPlayer() { + return playerSpawn.enabled() + ? playerSpawn.getLocation() + : teleport.getLocation(); + } + + // --- Builder --- + public static class Builder { private final String name; private RegionConfig region; @@ -39,7 +110,7 @@ public final class MineConfigDefaults { private WorldGuardConfig worldGuard = WorldGuardConfig.disabled(); private PlayerSpawnConfig playerSpawn = PlayerSpawnConfig.disabled(); - public Builder(String name) { + private Builder(String name) { this.name = name; } diff --git a/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java b/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java index 2fa8a06..fb3cbe6 100644 --- a/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java @@ -1,7 +1,5 @@ package dev.loki.lomines.data.config.block; -import org.bukkit.Material; - import java.util.Collections; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java b/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java index 31d552b..eecce4e 100644 --- a/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java +++ b/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java @@ -10,6 +10,12 @@ import java.util.Objects; */ public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, BlockKey.ItemsAdder { + /** + * Serializes this key to string format for YAML storage. + * Format: "stone" (vanilla), "oraxen:my_block", "itemsadder:custom_block" + */ + String serialize(); + /** * Deserializes a string to appropriate BlockKey type. * @@ -37,12 +43,6 @@ public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, Bloc return new Vanilla(material); } - /** - * Serializes this key to string format for YAML storage. - * Format: "stone" (vanilla), "oraxen:my_block", "itemsadder:custom_block" - */ - String serialize(); - // --- Implementations --- record Vanilla(Material material) implements BlockKey { diff --git a/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java index 61b8e24..352d471 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java @@ -32,9 +32,7 @@ public final class BlockConfigLoader { Map weights = new HashMap<>(); for (String key : contents.getKeys(false)) { double weight = contents.getDouble(key); - if (weight <= 0) { - continue; - } + if (weight <= 0) continue; weights.put(BlockKey.deserialize(key), weight); } @@ -42,23 +40,22 @@ public final class BlockConfigLoader { yaml.getString("blocks.fill-mode", "CUBOID").toUpperCase() ); - return new BlockConfig(weights, fillMode, parseMaskConfig(yaml, fillMode)); - } + // Parse mask config if applicable + BlockConfig.MaskConfig mask = null; + if (fillMode == FillMode.MASK) { + String markerStr = yaml.getString("blocks.mask.marker", "pink_concrete"); + BlockKey marker = BlockKey.deserialize(markerStr); - private BlockConfig.MaskConfig parseMaskConfig(YamlConfiguration yaml, FillMode fillMode) { - if (fillMode != FillMode.MASK) { - return null; - } - String markerStr = yaml.getString("blocks.mask.marker", "pink_concrete"); - BlockKey marker = BlockKey.deserialize(markerStr); + Map positions = new HashMap<>(); + List posList = yaml.getStringList("blocks.mask.positions"); + for (String pos : posList) { + positions.put(pos, true); + } - Map positions = new HashMap<>(); - List posList = yaml.getStringList("blocks.mask.positions"); - for (String pos : posList) { - positions.put(pos, true); + mask = new BlockConfig.MaskConfig(marker, positions); } - return new BlockConfig.MaskConfig(marker, positions); + return new BlockConfig(weights, fillMode, mask); } /** diff --git a/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java index b3eaf94..409407e 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java @@ -17,17 +17,32 @@ public final class RegionConfigLoader { /** * Parses region configuration from YAML. - * Supports up to 100 selection points (50 regions). */ public RegionConfig parse(YamlConfiguration yaml) { List selections = new ArrayList<>(); - // Try new format first (region.selection.X) - selections.addAll(parseSelections(yaml, "region.selection.", 100)); + // Try new format first + for (int i = 1; i <= 10; i++) { + String key = "region.selection." + i; + if (yaml.contains(key)) { + String locStr = yaml.getString(key); + if (locStr != null && !locStr.isBlank()) { + selections.add(parseLocation(locStr)); + } + } + } - // Legacy fallback (selection.X) + // Legacy fallback if (selections.isEmpty()) { - selections.addAll(parseSelections(yaml, "selection.", 100)); + for (int i = 1; i <= 10; i++) { + String key = "selection." + i; + if (yaml.contains(key)) { + String locStr = yaml.getString(key); + if (locStr != null && !locStr.isBlank()) { + selections.add(parseLocation(locStr)); + } + } + } } if (selections.size() < 2) { @@ -37,56 +52,15 @@ public final class RegionConfigLoader { return RegionConfig.fromSelections(selections); } - /** - * Parses selection points from YAML with given prefix. - * Supports indexed format: prefix.1, prefix.2, etc. - */ - private List parseSelections(YamlConfiguration yaml, String prefix, int maxPoints) { - List selections = new ArrayList<>(); - - for (int i = 1; i <= maxPoints; i++) { - String key = prefix + i; - if (yaml.contains(key)) { - String locStr = yaml.getString(key); - if (locStr != null && !locStr.isBlank()) { - try { - selections.add(parseLocation(locStr)); - } catch (IllegalArgumentException e) { - // Skip invalid locations but continue parsing - } - } - } else { - // Stop at first missing index (assuming sequential) - // But check at least a few more for gaps - if (i > 20) { - boolean hasMore = false; - for (int j = i + 1; j <= Math.min(i + 5, maxPoints); j++) { - if (yaml.contains(prefix + j)) { - hasMore = true; - break; - } - } - if (!hasMore) { - break; - } - } - } - } - - return selections; - } - /** * Saves region configuration to YAML. */ public void save(YamlConfiguration yaml, RegionConfig region) { int i = 1; for (var cuboid : region.regions()) { - Location min = new Location(cuboid.getWorld(), cuboid.getMinX(), cuboid.getMinY(), cuboid.getMinZ()); - yaml.set("region.selection." + i, LocationParser.format(min)); + yaml.set("region.selection." + i, LocationParser.format(cuboid.getMin())); i++; - Location max = new Location(cuboid.getWorld(), cuboid.getMaxX(), cuboid.getMaxY(), cuboid.getMaxZ()); - yaml.set("region.selection." + i, LocationParser.format(max)); + yaml.set("region.selection." + i, LocationParser.format(cuboid.getMax())); i++; } } diff --git a/src/main/java/dev/loki/lomines/data/config/loader/region/WorldGuardConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/region/WorldGuardConfigLoader.java index 7be99d1..021da2b 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/region/WorldGuardConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/region/WorldGuardConfigLoader.java @@ -1,10 +1,14 @@ package dev.loki.lomines.data.config.loader.region; -import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig; +import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.integration.worldguard.WorldGuardConfig; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Loader for WorldGuard configuration section. @@ -19,72 +23,56 @@ public final class WorldGuardConfigLoader { /** * Parses WorldGuard configuration from YAML. - * Supports any WorldGuard flags. Default: block-break=allow. */ public WorldGuardConfig parse(YamlConfiguration yaml) { - if (!isEnabled(yaml)) { + ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard"); + if (wgSection == null) { + // Check defaults + boolean defaultEnabled = defaults.getBoolean("worldguard.enabled", false); + if (!defaultEnabled) { + return WorldGuardConfig.disabled(); + } + } + + boolean enabled = yaml.getBoolean("worldguard.enabled", + defaults.getBoolean("worldguard.enabled", false)); + + if (!enabled) { return WorldGuardConfig.disabled(); } - List owners = resolveOwners(yaml); - List members = resolveMembers(yaml); - List flags = resolveFlags(yaml); - boolean protect = resolveProtect(yaml); - String tmpl = yaml.getString("worldguard.region-template", + String template = yaml.getString("worldguard.region-template", defaults.getString("worldguard.region-template", "{mine_name}_{random_4}")); - return WorldGuardConfig.builder() - .enabled(true) - .template(tmpl) - .owners(owners) - .members(members) - .flags(flags) - .protectOnCreate(protect) - .build(); - } - - private boolean isEnabled(YamlConfiguration yaml) { - ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard"); - if (wgSection == null) { - boolean defaultEnabled = defaults.getBoolean("worldguard.enabled", false); - if (!defaultEnabled) { - return false; - } - } - return yaml.getBoolean("worldguard.enabled", defaults.getBoolean("worldguard.enabled", false)); - } - - private List resolveOwners(YamlConfiguration yaml) { List owners = yaml.getStringList("worldguard.owners"); if (owners.isEmpty()) { owners = defaults.getStringList("worldguard.owners"); } - return owners; - } - private List resolveMembers(YamlConfiguration yaml) { List members = yaml.getStringList("worldguard.members"); if (members.isEmpty()) { members = defaults.getStringList("worldguard.members"); } - return members; - } - private List resolveFlags(YamlConfiguration yaml) { - // Support any WorldGuard flags. Default: only block-break=allow List flags = yaml.getStringList("worldguard.flags"); if (flags.isEmpty()) { flags = defaults.getStringList("worldguard.flags"); } if (flags.isEmpty()) { - flags = List.of("block-break=allow"); + flags = List.of("passthrough=deny", "build=allow"); } - return flags; - } - private boolean resolveProtect(YamlConfiguration yaml) { - return yaml.getBoolean("worldguard.protect-on-create", + boolean protect = yaml.getBoolean("worldguard.protect-on-create", defaults.getBoolean("worldguard.protect-on-create", true)); + + return WorldGuardConfig.builder() + .enabled(true) + .template(template) + .owners(owners) + .members(members) + .flags(flags) + .protectOnCreate(protect) + .build(); } /** @@ -104,14 +92,13 @@ public final class WorldGuardConfigLoader { /** * Sets default values for WorldGuard configuration. - * Default: only block-break=allow. User can add any flags. */ public void setDefaults(YamlConfiguration yaml) { yaml.set("worldguard.enabled", false); yaml.set("worldguard.region-template", "{mine_name}_{random_4}"); yaml.set("worldguard.owners", List.of()); yaml.set("worldguard.members", List.of()); - yaml.set("worldguard.flags", List.of("block-break=allow")); + yaml.set("worldguard.flags", List.of("passthrough=deny", "build=allow")); yaml.set("worldguard.protect-on-create", true); } } diff --git a/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java index e4b149a..bb4defd 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java @@ -36,31 +36,18 @@ public final class RewardConfigLoader { } private RewardEntry parseRewardEntry(Map map) { - List blocks = parseBlocks(map.get("blocks")); - List items = parseItems(map.get("items")); - List commands = parseCommands(map.get("commands")); - Object preventDropsObj = map.get("prevent-drops"); - boolean preventDrops = preventDropsObj instanceof Boolean b && b; + double chance = ((Number) map.getOrDefault("chance", 0)).doubleValue(); - Object chanceObj = map.get("chance"); - double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0; - return new RewardEntry(blocks, chance, items, commands, preventDrops); - } - - private List parseBlocks(Object rawBlocks) { List blocks = new ArrayList<>(); - List blockList = (List) rawBlocks; + List blockList = (List) map.get("blocks"); if (blockList != null) { for (Object b : blockList) { blocks.add(BlockKey.deserialize(b.toString())); } } - return blocks; - } - private List parseItems(Object rawItems) { List items = new ArrayList<>(); - List itemList = (List) rawItems; + List itemList = (List) map.get("items"); if (itemList != null) { for (Object i : itemList) { @SuppressWarnings("unchecked") @@ -68,19 +55,12 @@ public final class RewardConfigLoader { items.add(parseItemReward(itemMap)); } } - return items; - } - private List parseCommands(Object commandsObj) { - List commands = new ArrayList<>(); - if (commandsObj instanceof List list) { - for (Object obj : list) { - if (obj != null) { - commands.add(obj.toString()); - } - } - } - return commands; + @SuppressWarnings("unchecked") + List commands = (List) map.getOrDefault("commands", List.of()); + boolean preventDrops = (Boolean) map.getOrDefault("prevent-drops", false); + + return new RewardEntry(blocks, chance, items, commands, preventDrops); } private ItemReward parseItemReward(Map map) { diff --git a/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java index 8667025..d939529 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java @@ -3,6 +3,8 @@ package dev.loki.lomines.data.config.loader.system; import dev.loki.lomines.data.config.reset.ResetConfig; import org.bukkit.configuration.file.YamlConfiguration; +import java.util.List; + /** * Loader for reset configuration section. */ diff --git a/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java index 12cae6c..d8ba26b 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java @@ -1,9 +1,7 @@ package dev.loki.lomines.data.config.loader.system; import dev.loki.lomines.data.config.ui.UIConfig; -import dev.loki.lomines.data.config.ui.HologramConfig; import org.bukkit.configuration.file.YamlConfiguration; -import java.util.List; /** * Loader for UI configuration section. @@ -27,21 +25,10 @@ public final class UIConfigLoader { actionBarEnabled, yaml.getString("ui.actionbar.format", UIConfig.DEFAULT_ACTIONBAR_FORMAT), yaml.getDouble("ui.actionbar.range", 50.0), - yaml.getString("ui.timer-format", UIConfig.DEFAULT_TIMER_FORMAT), - parseHologram(yaml) + yaml.getString("ui.timer-format", UIConfig.DEFAULT_TIMER_FORMAT) ); } - private HologramConfig parseHologram(YamlConfiguration yaml) { - boolean enabled = yaml.getBoolean("ui.hologram.enabled", false); - List format = yaml.getStringList("ui.hologram.format"); - if (format.isEmpty()) { - format = HologramConfig.defaults().format(); - } - double height = yaml.getDouble("ui.hologram.height", 2.5); - return new HologramConfig(enabled, format, height); - } - /** * Saves UI configuration to YAML. */ @@ -50,8 +37,5 @@ public final class UIConfigLoader { yaml.set("ui.actionbar.format", ui.actionBarFormat()); yaml.set("ui.actionbar.range", ui.actionBarRange()); yaml.set("ui.timer-format", ui.timerFormat()); - yaml.set("ui.hologram.enabled", ui.hologram().enabled()); - yaml.set("ui.hologram.format", ui.hologram().format()); - yaml.set("ui.hologram.height", ui.hologram().height()); } } diff --git a/src/main/java/dev/loki/lomines/data/config/model/MineConfig.java b/src/main/java/dev/loki/lomines/data/config/model/MineConfig.java deleted file mode 100644 index 14b1638..0000000 --- a/src/main/java/dev/loki/lomines/data/config/model/MineConfig.java +++ /dev/null @@ -1,64 +0,0 @@ -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 getSpawnForStuckPlayer() { - return playerSpawn.enabled() - ? playerSpawn.getLocation() - : teleport.getLocation(); - } -} diff --git a/src/main/java/dev/loki/lomines/data/config/parser/ConfigParseException.java b/src/main/java/dev/loki/lomines/data/config/parser/ConfigParseException.java deleted file mode 100644 index d2bf87f..0000000 --- a/src/main/java/dev/loki/lomines/data/config/parser/ConfigParseException.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.loki.lomines.data.config.parser; - -/** - * Exception for configuration parsing errors. - */ -public class ConfigParseException extends Exception { - - public ConfigParseException(String message) { - super(message); - } - - public ConfigParseException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java b/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java index 0380f97..25dd60c 100644 --- a/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java @@ -1,6 +1,6 @@ package dev.loki.lomines.data.config.region; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.Cuboid; import org.bukkit.Location; import java.util.Collections; diff --git a/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java b/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java index bb14f83..c97ee17 100644 --- a/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java @@ -1,8 +1,13 @@ package dev.loki.lomines.data.config.reset; import java.time.Duration; +import java.util.Collections; import java.util.List; +import java.util.Objects; +/** + * Configuration for mine reset behavior. + */ public record ResetConfig( Duration interval, double percentTrigger, @@ -11,34 +16,41 @@ public record ResetConfig( String broadcastMessage ) { + public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5); + public static final double DEFAULT_PERCENT_TRIGGER = 10.0; + public ResetConfig { - interval = ResetConfigValidator.validateInterval(interval); - percentTrigger = ResetConfigValidator.validatePercentTrigger(percentTrigger); + // Normalize and validate interval + if (interval == null || interval.isZero() || interval.isNegative()) { + 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(); broadcastMessage = broadcastMessage != null ? broadcastMessage : ""; } - public static final Duration DEFAULT_INTERVAL = ResetConfigValidator.DEFAULT_INTERVAL; - public static final double DEFAULT_PERCENT_TRIGGER = ResetConfigValidator.DEFAULT_PERCENT_TRIGGER; - - public static ResetConfig defaults() { - return new ResetConfig( - DEFAULT_INTERVAL, - DEFAULT_PERCENT_TRIGGER, - false, - List.of(), - "" - ); - } - - public static Builder builder() { - return new Builder(); - } - + /** + * Interval in ticks (20 ticks = 1 second). + */ public long intervalTicks() { return interval.getSeconds() * 20; } + /** + * Human-readable interval string. + */ public String intervalDisplay() { long seconds = interval.getSeconds(); if (seconds < 60) { @@ -50,10 +62,33 @@ public record ResetConfig( } } + /** + * Whether reset should trigger based on percent mined. + */ public boolean isPercentTriggerEnabled() { return percentEnabled && percentTrigger > 0; } + /** + * Default config: 5 minutes, 10% trigger disabled. + */ + public static ResetConfig defaults() { + return new ResetConfig( + DEFAULT_INTERVAL, + DEFAULT_PERCENT_TRIGGER, + false, + List.of(), + "" + ); + } + + /** + * Builder for fluent construction. + */ + public static Builder builder() { + return new Builder(); + } + public static class Builder { private Duration interval = DEFAULT_INTERVAL; private double percentTrigger = DEFAULT_PERCENT_TRIGGER; @@ -66,13 +101,13 @@ public record ResetConfig( return this; } - public Builder interval(String humanReadable) { - this.interval = ResetConfigValidator.parseDuration(humanReadable); + public Builder intervalTicks(long ticks) { + this.interval = Duration.ofSeconds(ticks / 20); return this; } - public Builder intervalTicks(long ticks) { - this.interval = Duration.ofSeconds(ticks / 20); + public Builder interval(String humanReadable) { + this.interval = parseDuration(humanReadable); return this; } @@ -99,5 +134,25 @@ public record ResetConfig( public ResetConfig build() { return new ResetConfig(interval, percentTrigger, percentEnabled, commands, broadcastMessage); } + + private static Duration parseDuration(String s) { + s = s.trim().toLowerCase(); + try { + if (s.endsWith("s")) { + return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1))); + } else if (s.endsWith("m")) { + return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1))); + } else if (s.endsWith("h")) { + return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1))); + } else if (s.endsWith("d")) { + return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1))); + } else { + // Assume seconds if no suffix + return Duration.ofSeconds(Long.parseLong(s)); + } + } catch (NumberFormatException e) { + return DEFAULT_INTERVAL; + } + } } } diff --git a/src/main/java/dev/loki/lomines/data/config/reset/ResetConfigValidator.java b/src/main/java/dev/loki/lomines/data/config/reset/ResetConfigValidator.java deleted file mode 100644 index 6f3d5d4..0000000 --- a/src/main/java/dev/loki/lomines/data/config/reset/ResetConfigValidator.java +++ /dev/null @@ -1,50 +0,0 @@ -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; - } - } -} diff --git a/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java b/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java index 1cecb4c..9d8c2b7 100644 --- a/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java @@ -4,7 +4,9 @@ import dev.loki.lomines.data.config.block.BlockKey; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -16,20 +18,6 @@ public record RewardConfig(List entries) { entries = entries != null ? List.copyOf(entries) : List.of(); } - /** - * Empty reward config. - */ - public static RewardConfig empty() { - return new RewardConfig(List.of()); - } - - /** - * Single entry reward config builder. - */ - public static Builder builder() { - return new Builder(); - } - /** * Returns rewards applicable for the given block. */ @@ -46,6 +34,20 @@ public record RewardConfig(List entries) { return !entries.isEmpty(); } + /** + * Empty reward config. + */ + public static RewardConfig empty() { + return new RewardConfig(List.of()); + } + + /** + * Single entry reward config builder. + */ + public static Builder builder() { + return new Builder(); + } + // --- Reward Entry --- public record RewardEntry( diff --git a/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java index f3ef227..8cecd00 100644 --- a/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java @@ -2,36 +2,35 @@ package dev.loki.lomines.data.config.spawn; import org.bukkit.Location; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import java.util.Optional; /** - * Configuration for player spawn locations when stuck in blocks. - * Supports multiple spawn points for a single mine. + * Configuration for player spawn location when stuck in blocks. * Used to teleport players out when they are suffocating after mine reset. */ -public record PlayerSpawnConfig(boolean enabled, List locations) { +public record PlayerSpawnConfig( + boolean enabled, + Location location +) { - public PlayerSpawnConfig(boolean enabled, List locations) { - this.enabled = enabled; - if (!enabled || locations == null) { - this.locations = List.of(); - } else { - // Defensive copy with validation - List copy = new ArrayList<>(); - for (Location loc : locations) { - if (loc != null) { - Location cloned = loc.clone(); - Objects.requireNonNull(cloned.getWorld(), "Spawn world cannot be null"); - copy.add(cloned); - } - } - this.locations = List.copyOf(copy); + public PlayerSpawnConfig { + if (!enabled) { + location = null; + } else if (location != null) { + // Defensive copy + location = location.clone(); + Objects.requireNonNull(location.getWorld(), "Spawn world cannot be null"); } } + /** + * Returns the spawn location if enabled. + */ + public Optional getLocation() { + return Optional.ofNullable(enabled ? location : null); + } + /** * Disabled spawn config (default). */ @@ -40,74 +39,9 @@ public record PlayerSpawnConfig(boolean enabled, List locations) { } /** - * Creates enabled config with single location. + * Creates enabled config with location. */ public static PlayerSpawnConfig at(Location location) { - return new PlayerSpawnConfig(true, List.of(location)); - } - - /** - * Creates enabled config with multiple locations. - */ - public static PlayerSpawnConfig at(List locations) { - return new PlayerSpawnConfig(true, locations); - } - - /** - * Returns the primary spawn location (first in list) if enabled. - */ - public Optional getLocation() { - return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0)); - } - - /** - * Returns all spawn locations. - */ - @Override - public List locations() { - return locations; - } - - /** - * Returns true if spawn is enabled and has at least one location. - */ - @Override - public boolean enabled() { - return enabled && !locations.isEmpty(); - } - - /** - * Returns the number of configured spawn locations. - */ - public int count() { - return locations.size(); - } - - /** - * Creates a new config with an additional location. - */ - public PlayerSpawnConfig addLocation(Location location) { - List newList = new ArrayList<>(locations); - newList.add(location); - return new PlayerSpawnConfig(true, newList); - } - - /** - * Creates a new config without the location at given index. - */ - public PlayerSpawnConfig removeLocation(int index) { - if (index < 0 || index >= locations.size()) { - return this; - } - List newList = new ArrayList<>(locations); - newList.remove(index); - return new PlayerSpawnConfig(!newList.isEmpty(), newList); - } - - /** - * Clears all locations (disables spawn). - */ - public PlayerSpawnConfig clear() { - return disabled(); + return new PlayerSpawnConfig(true, location); } } diff --git a/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java b/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java index 615b3bb..c468b63 100644 --- a/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java @@ -2,35 +2,34 @@ package dev.loki.lomines.data.config.teleport; import org.bukkit.Location; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import java.util.Optional; /** * Configuration for mine teleportation on reset. - * Supports multiple teleport locations for a single mine. */ -public record TeleportConfig(boolean enabled, List locations) { +public record TeleportConfig( + boolean enabled, + Location location +) { - public TeleportConfig(boolean enabled, List locations) { - this.enabled = enabled; - if (!enabled || locations == null) { - this.locations = List.of(); - } else { - // Defensive copy with validation - List copy = new ArrayList<>(); - for (Location loc : locations) { - if (loc != null) { - Location cloned = loc.clone(); - Objects.requireNonNull(cloned.getWorld(), "Teleport world cannot be null"); - copy.add(cloned); - } - } - this.locations = List.copyOf(copy); + public TeleportConfig { + if (!enabled) { + location = null; + } else if (location != null) { + // Defensive copy + location = location.clone(); + Objects.requireNonNull(location.getWorld(), "Teleport world cannot be null"); } } + /** + * Returns the teleport location if enabled. + */ + public Optional getLocation() { + return Optional.ofNullable(enabled ? location : null); + } + /** * Disabled teleport config. */ @@ -39,74 +38,9 @@ public record TeleportConfig(boolean enabled, List locations) { } /** - * Creates enabled config with single location. + * Creates enabled config with location. */ public static TeleportConfig at(Location location) { - return new TeleportConfig(true, List.of(location)); - } - - /** - * Creates enabled config with multiple locations. - */ - public static TeleportConfig at(List locations) { - return new TeleportConfig(true, locations); - } - - /** - * Returns the primary teleport location (first in list) if enabled. - */ - public Optional getLocation() { - return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0)); - } - - /** - * Returns all teleport locations. - */ - @Override - public List locations() { - return locations; - } - - /** - * Returns true if teleport is enabled and has at least one location. - */ - @Override - public boolean enabled() { - return enabled && !locations.isEmpty(); - } - - /** - * Returns the number of configured teleport locations. - */ - public int count() { - return locations.size(); - } - - /** - * Creates a new config with an additional location. - */ - public TeleportConfig addLocation(Location location) { - List newList = new ArrayList<>(locations); - newList.add(location); - return new TeleportConfig(true, newList); - } - - /** - * Creates a new config without the location at given index. - */ - public TeleportConfig removeLocation(int index) { - if (index < 0 || index >= locations.size()) { - return this; - } - List newList = new ArrayList<>(locations); - newList.remove(index); - return new TeleportConfig(!newList.isEmpty(), newList); - } - - /** - * Clears all locations (disables teleport). - */ - public TeleportConfig clear() { - return disabled(); + return new TeleportConfig(true, location); } } diff --git a/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java b/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java index bdfbcd7..e829ef3 100644 --- a/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java @@ -6,8 +6,8 @@ import java.util.List; * Configuration for holograms displayed above mines. * * @param enabled whether holograms are enabled - * @param format list of lines with placeholders {mine}, {percent}, {bar}, {time} - * @param height offset above the mine region (in blocks) + * @param format list of lines with placeholders {mine}, {percent}, {bar}, {time} + * @param height offset above the mine region (in blocks) */ public record HologramConfig( boolean enabled, diff --git a/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java b/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java index e4bcfc2..c3eb373 100644 --- a/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java @@ -1,9 +1,9 @@ package dev.loki.lomines.data.config.ui; -import dev.loki.lomines.util.format.color.ColorUtils; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.Component; -import java.util.Locale; +import java.util.Objects; /** * Configuration for UI elements (action bar, messages). @@ -16,11 +16,14 @@ public record UIConfig( HologramConfig hologram ) { + public UIConfig { + hologram = hologram != null ? hologram : HologramConfig.disabled(); + } + public static final String DEFAULT_ACTIONBAR_FORMAT = "{mine} {percent}% ({time})"; public static final String DEFAULT_TIMER_FORMAT = "mm:ss"; public UIConfig { - hologram = hologram != null ? hologram : HologramConfig.disabled(); actionBarFormat = actionBarFormat != null && !actionBarFormat.isBlank() ? actionBarFormat : DEFAULT_ACTIONBAR_FORMAT; @@ -30,34 +33,19 @@ public record UIConfig( actionBarRange = Math.max(1, actionBarRange); } - /** - * Default configuration. - */ - public static UIConfig defaults() { - return new UIConfig(true, DEFAULT_ACTIONBAR_FORMAT, 50.0, DEFAULT_TIMER_FORMAT, HologramConfig.defaults()); - } - - /** - * Disabled configuration (no UI). - */ - public static UIConfig disabled() { - return new UIConfig(false, "", 0, DEFAULT_TIMER_FORMAT, HologramConfig.disabled()); - } - /** * Parses the action bar format with placeholders. * Placeholders: {mine}, {percent}, {time}, {blocks}, {total} - * Supports colors: &#RRGGBB, &a..&f, */ public Component formatActionBar(String mineName, double percent, String timeStr, int blocks, int total) { String parsed = actionBarFormat .replace("{mine}", mineName) - .replace("{percent}", String.format(Locale.US, "%.1f", percent)) + .replace("{percent}", String.format("%.1f", percent)) .replace("{time}", timeStr) .replace("{blocks}", String.valueOf(blocks)) .replace("{total}", String.valueOf(total)); - return ColorUtils.format(parsed); + return MiniMessage.miniMessage().deserialize(parsed); } /** @@ -82,4 +70,18 @@ public record UIConfig( public double actionBarRangeSquared() { return actionBarRange * actionBarRange; } + + /** + * Default configuration. + */ + public static UIConfig defaults() { + return new UIConfig(true, DEFAULT_ACTIONBAR_FORMAT, 50.0, DEFAULT_TIMER_FORMAT, HologramConfig.defaults()); + } + + /** + * Disabled configuration (no UI). + */ + public static UIConfig disabled() { + return new UIConfig(false, "", 0, DEFAULT_TIMER_FORMAT, HologramConfig.disabled()); + } } diff --git a/src/main/java/dev/loki/lomines/data/reward/entity/Reward.java b/src/main/java/dev/loki/lomines/data/reward/Reward.java similarity index 83% rename from src/main/java/dev/loki/lomines/data/reward/entity/Reward.java rename to src/main/java/dev/loki/lomines/data/reward/Reward.java index 7b15d36..9b5bed4 100644 --- a/src/main/java/dev/loki/lomines/data/reward/entity/Reward.java +++ b/src/main/java/dev/loki/lomines/data/reward/Reward.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward.entity; +package dev.loki.lomines.data.reward; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -49,12 +49,8 @@ public record Reward(List materials, double chance, List it @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; Reward reward = (Reward) o; return Double.compare(reward.chance, chance) == 0 && preventDrops == reward.preventDrops && @@ -63,8 +59,4 @@ public record Reward(List materials, double chance, List it Objects.equals(commands, reward.commands); } - @Override - public int hashCode() { - return Objects.hash(materials, chance, items, commands, preventDrops); - } } diff --git a/src/main/java/dev/loki/lomines/data/reward/parse/command/RewardCommandParser.java b/src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java similarity index 84% rename from src/main/java/dev/loki/lomines/data/reward/parse/command/RewardCommandParser.java rename to src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java index d058411..a93ab1b 100644 --- a/src/main/java/dev/loki/lomines/data/reward/parse/command/RewardCommandParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward.parse.command; +package dev.loki.lomines.data.reward; import dev.loki.lomines.data.config.parser.ConfigParseException; @@ -9,10 +9,10 @@ import java.util.Map; /** * Parses reward commands from configuration. */ -public final class RewardCommandParser { +final class RewardCommandParser { @SuppressWarnings("unchecked") - public List parseCommands(Map rewardMap) throws ConfigParseException { + List parseCommands(Map rewardMap) throws ConfigParseException { List commands = new ArrayList<>(); if (!rewardMap.containsKey("commands")) { return commands; diff --git a/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardItemParser.java b/src/main/java/dev/loki/lomines/data/reward/RewardItemParser.java similarity index 93% rename from src/main/java/dev/loki/lomines/data/reward/parse/item/RewardItemParser.java rename to src/main/java/dev/loki/lomines/data/reward/RewardItemParser.java index 49c7d7e..b52bf30 100644 --- a/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardItemParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/RewardItemParser.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward.parse.item; +package dev.loki.lomines.data.reward; import dev.loki.lomines.data.config.parser.ConfigParseException; import dev.loki.lomines.util.ValidationUtils; @@ -12,7 +12,7 @@ import java.util.Map; * Parses ItemStack objects from YAML reward item configurations. * Handles material type, amount, and basic item properties. */ -public final class RewardItemParser { +final class RewardItemParser { /** * Parses a single reward item from a map configuration. @@ -22,7 +22,7 @@ public final class RewardItemParser { * @return the parsed ItemStack * @throws ConfigParseException if the item configuration is invalid */ - public ItemStack parseItem(Map itemMap, int index) throws ConfigParseException { + ItemStack parseItem(Map itemMap, int index) throws ConfigParseException { if (!itemMap.containsKey("type")) { throw new ConfigParseException("Missing required 'type' field in item at index " + index); } diff --git a/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardMaterialParser.java b/src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java similarity index 88% rename from src/main/java/dev/loki/lomines/data/reward/parse/item/RewardMaterialParser.java rename to src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java index 1085935..9850d04 100644 --- a/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardMaterialParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward.parse.item; +package dev.loki.lomines.data.reward; import dev.loki.lomines.data.config.parser.ConfigParseException; import dev.loki.lomines.util.ValidationUtils; @@ -11,10 +11,10 @@ import java.util.Map; /** * Parses reward materials (blocks) from configuration. */ -public final class RewardMaterialParser { +final class RewardMaterialParser { @SuppressWarnings("unchecked") - public List parseMaterials(Map rewardMap) throws ConfigParseException { + List parseMaterials(Map rewardMap) throws ConfigParseException { if (!rewardMap.containsKey("blocks")) { throw new ConfigParseException("Missing required 'blocks' field in reward"); } diff --git a/src/main/java/dev/loki/lomines/data/reward/parse/RewardEntryParser.java b/src/main/java/dev/loki/lomines/data/reward/RewardParser.java similarity index 65% rename from src/main/java/dev/loki/lomines/data/reward/parse/RewardEntryParser.java rename to src/main/java/dev/loki/lomines/data/reward/RewardParser.java index 44ced56..13ef724 100644 --- a/src/main/java/dev/loki/lomines/data/reward/parse/RewardEntryParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/RewardParser.java @@ -1,29 +1,69 @@ -package dev.loki.lomines.data.reward.parse; +package dev.loki.lomines.data.reward; import dev.loki.lomines.data.config.parser.ConfigParseException; -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.configuration.file.YamlConfiguration; import org.bukkit.inventory.ItemStack; import java.util.ArrayList; import java.util.List; 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 RewardMaterialParser materialParser; private final RewardCommandParser commandParser; - public RewardEntryParser() { + public RewardParser() { this.itemParser = new RewardItemParser(); this.materialParser = new RewardMaterialParser(); this.commandParser = new RewardCommandParser(); } - public Reward parseReward(Map rewardMap) throws ConfigParseException { + public List parseRewards(YamlConfiguration yaml) throws ConfigParseException { + List 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 rewardMap = (Map) 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 rewardMap) throws ConfigParseException { double chance = parseRewardField(rewardMap, "chance", Double.class); boolean preventDrops = parseRewardField(rewardMap, "prevent-drops", Boolean.class, false); @@ -35,7 +75,7 @@ public final class RewardEntryParser { } @SuppressWarnings("unchecked") - public T parseRewardField(Map map, String key, Class type) + private T parseRewardField(Map map, String key, Class type) throws ConfigParseException { if (!map.containsKey(key)) { throw new ConfigParseException("Missing required '" + key + "' field in reward"); @@ -56,7 +96,7 @@ public final class RewardEntryParser { } @SuppressWarnings("unchecked") - public T parseRewardField(Map map, String key, Class type, T defaultValue) { + private T parseRewardField(Map map, String key, Class type, T defaultValue) { if (!map.containsKey(key)) { return defaultValue; } @@ -67,7 +107,7 @@ public final class RewardEntryParser { return defaultValue; } - public Double parseDoubleValue(String key, Object value) throws ConfigParseException { + private Double parseDoubleValue(String key, Object value) throws ConfigParseException { try { if (value instanceof Number) { return ((Number) value).doubleValue(); @@ -84,7 +124,7 @@ public final class RewardEntryParser { } } - public List parseRewardItems(Map rewardMap) + private List parseRewardItems(Map rewardMap) throws ConfigParseException { List items = new ArrayList<>(); if (!rewardMap.containsKey("items")) { diff --git a/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java b/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java deleted file mode 100644 index 7752d2f..0000000 --- a/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java +++ /dev/null @@ -1,58 +0,0 @@ -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 parseRewards(YamlConfiguration yaml) throws ConfigParseException { - List rewards = new ArrayList<>(); - - if (!yaml.contains("random-rewards")) { - return rewards; - } - - List rewardsList = yaml.getList("random-rewards"); - if (rewardsList == null) { - return rewards; - } - - for (int i = 0; i < rewardsList.size(); i++) { - rewards.add(parseRewardAt(rewardsList.get(i), i)); - } - - return rewards; - } - - private Reward parseRewardAt(Object rewardObj, int i) throws ConfigParseException { - if (!(rewardObj instanceof Map)) { - throw new ConfigParseException( - "Invalid reward at index " + i + ": expected map, got " + - (rewardObj != null ? rewardObj.getClass().getSimpleName() : "null") - ); - } - - @SuppressWarnings("unchecked") - Map rewardMap = (Map) rewardObj; - - try { - return entryParser.parseReward(rewardMap); - } catch (ConfigParseException e) { - throw new ConfigParseException( - "Error parsing reward at index " + i + ": " + e.getMessage(), - e - ); - } - } -} diff --git a/src/main/java/dev/loki/lomines/data/stats/model/Leaderboard.java b/src/main/java/dev/loki/lomines/data/stats/Leaderboard.java similarity index 97% rename from src/main/java/dev/loki/lomines/data/stats/model/Leaderboard.java rename to src/main/java/dev/loki/lomines/data/stats/Leaderboard.java index 8d9d9e1..710e2b1 100644 --- a/src/main/java/dev/loki/lomines/data/stats/model/Leaderboard.java +++ b/src/main/java/dev/loki/lomines/data/stats/Leaderboard.java @@ -1,6 +1,4 @@ -package dev.loki.lomines.data.stats.model; - -import dev.loki.lomines.data.stats.service.StatsManager; +package dev.loki.lomines.data.stats; import java.util.Comparator; import java.util.List; diff --git a/src/main/java/dev/loki/lomines/data/stats/model/LeaderboardEntry.java b/src/main/java/dev/loki/lomines/data/stats/LeaderboardEntry.java similarity index 71% rename from src/main/java/dev/loki/lomines/data/stats/model/LeaderboardEntry.java rename to src/main/java/dev/loki/lomines/data/stats/LeaderboardEntry.java index f6f0cb3..810ebba 100644 --- a/src/main/java/dev/loki/lomines/data/stats/model/LeaderboardEntry.java +++ b/src/main/java/dev/loki/lomines/data/stats/LeaderboardEntry.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.stats.model; +package dev.loki.lomines.data.stats; import java.util.Objects; import java.util.UUID; @@ -16,21 +16,12 @@ public record LeaderboardEntry(UUID playerId, long count) { @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; LeaderboardEntry that = (LeaderboardEntry) o; return count == that.count && playerId.equals(that.playerId); } - @Override - public int hashCode() { - return Objects.hash(playerId, count); - } - @Override public String toString() { return "LeaderboardEntry{playerId=" + playerId + ", count=" + count + "}"; diff --git a/src/main/java/dev/loki/lomines/data/stats/model/PlayerStats.java b/src/main/java/dev/loki/lomines/data/stats/PlayerStats.java similarity index 97% rename from src/main/java/dev/loki/lomines/data/stats/model/PlayerStats.java rename to src/main/java/dev/loki/lomines/data/stats/PlayerStats.java index 2bd8f5d..91f1e78 100644 --- a/src/main/java/dev/loki/lomines/data/stats/model/PlayerStats.java +++ b/src/main/java/dev/loki/lomines/data/stats/PlayerStats.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.stats.model; +package dev.loki.lomines.data.stats; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/dev/loki/lomines/data/stats/StatsManager.java b/src/main/java/dev/loki/lomines/data/stats/StatsManager.java new file mode 100644 index 0000000..17a9e1e --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/stats/StatsManager.java @@ -0,0 +1,181 @@ +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 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 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 mineStats = playerStats.getMineStatsSnapshot(); + if (!mineStats.isEmpty()) { + for (Map.Entry 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 getAllStats() { + return Collections.unmodifiableCollection(stats.values()); + } + + /** + * Returns the leaderboard instance. + */ + public Leaderboard getLeaderboard() { + return leaderboard; + } +} diff --git a/src/main/java/dev/loki/lomines/data/stats/service/StatsManager.java b/src/main/java/dev/loki/lomines/data/stats/service/StatsManager.java deleted file mode 100644 index bed2f78..0000000 --- a/src/main/java/dev/loki/lomines/data/stats/service/StatsManager.java +++ /dev/null @@ -1,72 +0,0 @@ -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 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 getAllStats() { - return Collections.unmodifiableCollection(stats.values()); - } - - public Leaderboard getLeaderboard() { - return leaderboard; - } -} diff --git a/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java b/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java deleted file mode 100644 index 936c000..0000000 --- a/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java +++ /dev/null @@ -1,114 +0,0 @@ -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 stats) throws IOException { - if (!Files.exists(statsFile)) { - plugin.getLogger().info("Stats file does not exist, starting with empty statistics"); - return; - } - - File file = statsFile.toFile(); - YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file); - - ConfigurationSection playersSection = yaml.getConfigurationSection("players"); - if (playersSection == null) { - plugin.getLogger().info("No players section found in stats.yml"); - return; - } - - for (String uuidString : playersSection.getKeys(false)) { - loadPlayer(stats, uuidString, playersSection); - } - - plugin.getLogger().info("Loaded statistics for " + stats.size() + " player(s)"); - } - - private void loadPlayer(Map stats, String uuidString, ConfigurationSection playersSection) { - try { - UUID playerId = UUID.fromString(uuidString); - ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString); - - if (playerSection == null) { - return; - } - - PlayerStats playerStats = stats.computeIfAbsent(playerId, PlayerStats::new); - - long totalBlocks = playerSection.getLong("total", 0); - playerStats.setTotalBlocks(totalBlocks); - - ConfigurationSection minesSection = playerSection.getConfigurationSection("mines"); - if (minesSection != null) { - for (String mineName : minesSection.getKeys(false)) { - long mineBlocks = minesSection.getLong(mineName, 0); - playerStats.setMineBlocks(mineName, mineBlocks); - } - } - } catch (IllegalArgumentException e) { - plugin.getLogger().warning("Invalid UUID in stats.yml: " + uuidString); - } - } - - public void save(Map stats) throws IOException { - YamlConfiguration yaml = new YamlConfiguration(); - - for (Map.Entry entry : stats.entrySet()) { - UUID playerId = entry.getKey(); - PlayerStats playerStats = entry.getValue(); - - String path = "players." + playerId.toString(); - yaml.set(path + ".total", playerStats.getTotalBlocks()); - - Map mineStats = playerStats.getMineStatsSnapshot(); - if (!mineStats.isEmpty()) { - for (Map.Entry 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; - } - } -} diff --git a/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java b/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java index f29a415..352d383 100644 --- a/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java +++ b/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java @@ -13,12 +13,11 @@ import java.util.List; /** * Factory for creating ItemStacks with name and lore. */ -public final class ItemStackFactory { +final class ItemStackFactory { - private ItemStackFactory() { - } + private ItemStackFactory() {} - public static ItemStack create(Material mat, String name, String... loreLines) { + static ItemStack create(Material mat, String name, String... loreLines) { ItemStack stack = new ItemStack(mat); ItemMeta meta = stack.getItemMeta(); if (meta != null) { @@ -33,21 +32,17 @@ public final class ItemStackFactory { return stack; } - public static String loc(Location l) { - if (l == null || l.getWorld() == null) { - return "§7не задана"; - } + static String loc(Location l) { + 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()); } - public static String trunc(String s, int max) { - if (s == null || s.isEmpty()) { - return "§7нет"; - } + static String trunc(String s, int max) { + if (s == null || s.isEmpty()) return "§7нет"; return s.length() > max ? s.substring(0, max - 3) + "..." : s; } - public static ItemStack filler() { + static ItemStack filler() { ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemMeta meta = pane.getItemMeta(); if (meta != null) { diff --git a/src/main/java/dev/loki/lomines/gui/common/package-info.java b/src/main/java/dev/loki/lomines/gui/common/package-info.java new file mode 100644 index 0000000..5f05b16 --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/common/package-info.java @@ -0,0 +1,13 @@ +/** + * GUI editors for mine configuration. + *

+ * This package contains graphical user interface implementations using LoAPI's InventoryGUI: + *

    + *
  • {@code MinesEditor} - List view of all mines
  • + *
  • {@code MineEditor} - Main menu for a specific mine
  • + *
  • {@code MineContentsEditor} - Editor for mine block contents
  • + *
  • {@code MineRewardsEditor} - List view of mine rewards
  • + *
  • {@code MineRewardEditor} - Editor for a specific reward
  • + *
+ */ +package dev.loki.lomines.gui; diff --git a/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGui.java b/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGui.java index 4b7c5a2..ab8910e 100644 --- a/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGui.java +++ b/src/main/java/dev/loki/lomines/gui/confirm/ConfirmDeleteGui.java @@ -28,8 +28,8 @@ public final class ConfirmDeleteGui { /** * Opens the confirmation GUI for deleting a mine. * - * @param plugin the plugin instance - * @param player the player to open for + * @param plugin the plugin instance + * @param player the player to open for * @param mineName the name of the mine to delete */ public static void open(LoMinesPlugin plugin, Player player, String mineName) { diff --git a/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java b/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java index fb47b3d..a7445f2 100644 --- a/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java +++ b/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java @@ -1,7 +1,7 @@ package dev.loki.lomines.gui.group; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.core.mine.Mines; import dev.loki.lomines.wand.group.GroupWandManager; import dev.loki.lomines.wand.group.GroupWandSession; import net.kyori.adventure.text.Component; @@ -20,8 +20,7 @@ public final class GroupCreateGui { private static final int SIZE = 54; private static final int[] MINE_SLOTS = {10, 11, 12, 19, 20, 21, 28, 29, 30}; - private GroupCreateGui() { - } + private GroupCreateGui() {} public static void open(LoMinesPlugin plugin, Player player) { GroupCreateGuiHolder holder = new GroupCreateGuiHolder(player.getUniqueId()); @@ -94,28 +93,12 @@ public final class GroupCreateGui { return; } - CreationResult result = createSlots(plugin, player, session, base); - if (result.created() == 0) { - player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED)); - return; - } - player.sendMessage(Component.text( - "Создано шахт: " + result.created() + (result.skipped() > 0 ? " (пропущено: " + result.skipped() + ")" : ""), - NamedTextColor.GREEN)); - } - - private record CreationResult(int created, int skipped) { - } - - private static CreationResult createSlots(LoMinesPlugin plugin, Player player, GroupWandSession session, String base) { Mines mines = plugin.getMines(); int created = 0; int skipped = 0; for (int i = 0; i < 9; i++) { - if (!session.isSlotReady(i)) { - continue; - } + if (!session.isSlotReady(i)) continue; String mineName = base + "_" + (i + 1); try { mines.create(mineName, session.getPos1(i), session.getPos2(i)); @@ -125,10 +108,16 @@ public final class GroupCreateGui { skipped++; } catch (Exception e) { player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED)); - plugin.getLogger().severe("Group create failed for " + mineName); - e.printStackTrace(); + plugin.loLogger().error("Group create failed for " + mineName, e); } } - return new CreationResult(created, skipped); + + if (created == 0) { + player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED)); + return; + } + player.sendMessage(Component.text( + "Создано шахт: " + created + (skipped > 0 ? " (пропущено: " + skipped + ")" : ""), + NamedTextColor.GREEN)); } } diff --git a/src/main/java/dev/loki/lomines/gui/group/GroupCreateItems.java b/src/main/java/dev/loki/lomines/gui/group/GroupCreateItems.java index 6daa495..c42aaab 100644 --- a/src/main/java/dev/loki/lomines/gui/group/GroupCreateItems.java +++ b/src/main/java/dev/loki/lomines/gui/group/GroupCreateItems.java @@ -15,8 +15,7 @@ import java.util.List; */ final class GroupCreateItems { - private GroupCreateItems() { - } + private GroupCreateItems() {} static ItemStack filler() { ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/BlockMaterialSelectionGui.java b/src/main/java/dev/loki/lomines/gui/mine/edit/BlockMaterialSelectionGui.java new file mode 100644 index 0000000..e389b6c --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/mine/edit/BlockMaterialSelectionGui.java @@ -0,0 +1,178 @@ +package dev.loki.lomines.gui.mine.edit; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.gui.common.ItemStackFactory; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for selecting a material to add to mine blocks. + * Shows paginated list of available materials. + */ +public final class BlockMaterialSelectionGui { + + private static final int SIZE = 54; + private static final String TITLE = "Выберите материал"; + private static final int SLOT_BACK = 49; + private static final int SLOT_PREV = 45; + private static final int SLOT_NEXT = 53; + private static final int ITEMS_PER_PAGE = 45; + + private static final List AVAILABLE_MATERIALS = new ArrayList<>(); + + static { + // Initialize with common mine materials + for (Material mat : Material.values()) { + if (mat.isBlock() && !mat.isAir() && mat.isSolid()) { + AVAILABLE_MATERIALS.add(mat); + } + } + AVAILABLE_MATERIALS.sort((a, b) -> a.name().compareTo(b.name())); + } + + private BlockMaterialSelectionGui() {} + + public static void open(LoMinesPlugin plugin, Player player, String mineName) { + open(plugin, player, mineName, 0); + } + + public static void open(LoMinesPlugin plugin, Player player, String mineName, int page) { + BlockMaterialSelectionGuiHolder holder = new BlockMaterialSelectionGuiHolder( + player.getUniqueId(), mineName, page); + Inventory inv = Bukkit.createInventory(holder, + Component.text(TITLE + " (стр. " + (page + 1) + ")", NamedTextColor.DARK_GREEN)); + holder.setInventory(inv); + fill(inv, page); + player.openInventory(inv); + } + + private static void fill(Inventory inv, int page) { + // Fill background + for (int i = 0; i < SIZE; i++) { + inv.setItem(i, ItemStackFactory.filler()); + } + + // Calculate range for this page + int start = page * ITEMS_PER_PAGE; + int end = Math.min(start + ITEMS_PER_PAGE, AVAILABLE_MATERIALS.size()); + + // Fill materials + for (int i = 0; i < ITEMS_PER_PAGE && (start + i) < end; i++) { + Material mat = AVAILABLE_MATERIALS.get(start + i); + inv.setItem(i, materialItem(mat)); + } + + // Navigation buttons + if (page > 0) { + inv.setItem(SLOT_PREV, prevPageItem()); + } + if (end < AVAILABLE_MATERIALS.size()) { + inv.setItem(SLOT_NEXT, nextPageItem()); + } + + // Back button + inv.setItem(SLOT_BACK, backItem()); + } + + private static ItemStack materialItem(Material material) { + String name = formatMaterialName(material); + return ItemStackFactory.create(material, "§a§l" + name, + "§8───────────────", + "§7Нажмите для добавления", + "§7в конфигурацию шахты", + "", + "§8ID: §7" + material.name().toLowerCase() + ); + } + + private static String formatMaterialName(Material material) { + String name = material.name().toLowerCase().replace("_", " "); + StringBuilder result = new StringBuilder(); + for (String word : name.split(" ")) { + if (!word.isEmpty()) { + result.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)) + .append(" "); + } + } + return result.toString().trim(); + } + + private static ItemStack prevPageItem() { + return ItemStackFactory.create(Material.ARROW, "§e§l← Предыдущая", + "§8───────────────", + "§7Нажмите для перехода", + "§7на предыдущую страницу" + ); + } + + private static ItemStack nextPageItem() { + return ItemStackFactory.create(Material.ARROW, "§e§lСледующая →", + "§8───────────────", + "§7Нажмите для перехода", + "§7на следующую страницу" + ); + } + + private static ItemStack backItem() { + return ItemStackFactory.create(Material.BARRIER, "§c§lОтмена", + "§8───────────────", + "§7Вернуться без добавления" + ); + } + + public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, + String mineName, int page, boolean leftClick) { + if (rawSlot < 0 || rawSlot >= SIZE) return false; + + if (rawSlot == SLOT_BACK) { + BlocksGui.open(plugin, player, mineName); + return true; + } + + if (rawSlot == SLOT_PREV && page > 0) { + open(plugin, player, mineName, page - 1); + return true; + } + + if (rawSlot == SLOT_NEXT && (page + 1) * ITEMS_PER_PAGE < AVAILABLE_MATERIALS.size()) { + open(plugin, player, mineName, page + 1); + return true; + } + + // Material selected + if (rawSlot < ITEMS_PER_PAGE && leftClick) { + int index = page * ITEMS_PER_PAGE + rawSlot; + if (index < AVAILABLE_MATERIALS.size()) { + Material selected = AVAILABLE_MATERIALS.get(index); + addBlockToMine(plugin, player, mineName, selected); + } + return true; + } + + return true; + } + + private static void addBlockToMine(LoMinesPlugin plugin, Player player, + String mineName, Material material) { + player.sendMessage(Component.text("§aДобавлен блок: §f" + material.name().toLowerCase())); + player.sendMessage(Component.text("§7Вес установлен: §f10% (настройте в редакторе)")); + BlocksGui.open(plugin, player, mineName); + } + + public static Material getMaterialAtSlot(int slot, int page) { + int index = page * ITEMS_PER_PAGE + slot; + if (index >= 0 && index < AVAILABLE_MATERIALS.size()) { + return AVAILABLE_MATERIALS.get(index); + } + return null; + } +} diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/view/BlocksGui.java b/src/main/java/dev/loki/lomines/gui/mine/edit/BlocksGui.java similarity index 54% rename from src/main/java/dev/loki/lomines/gui/mine/edit/blocks/view/BlocksGui.java rename to src/main/java/dev/loki/lomines/gui/mine/edit/BlocksGui.java index cc86b49..7ef6eb5 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/view/BlocksGui.java +++ b/src/main/java/dev/loki/lomines/gui/mine/edit/BlocksGui.java @@ -1,14 +1,11 @@ -package dev.loki.lomines.gui.mine.edit.blocks.view; +package dev.loki.lomines.gui.mine.edit; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.gui.common.ItemStackFactory; -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.holder.BlocksGuiHolder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; @@ -19,6 +16,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +/** + * GUI for editing block weights in a mine. + */ public final class BlocksGui { private static final int SIZE = 54; @@ -26,8 +26,7 @@ public final class BlocksGui { private static final int SLOT_BACK = 49; private static final int SLOT_ADD_BLOCK = 52; - private BlocksGui() { - } + private BlocksGui() {} public static void open(LoMinesPlugin plugin, Player player, String mineName) { Mine mine = plugin.getMines().find(mineName).orElse(null); @@ -65,9 +64,7 @@ public final class BlocksGui { int slot = 0; for (Map.Entry entry : sortedEntries) { - if (slot >= 36) { - break; - } + if (slot >= 36) break; inv.setItem(slot, BlocksGuiItems.blockItem(entry.getKey(), entry.getValue())); slot++; } @@ -76,13 +73,10 @@ public final class BlocksGui { inv.setItem(SLOT_BACK, BlocksGuiItems.backItem()); } - @SuppressWarnings("checkstyle:ParameterNumber") public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, - String mineName, boolean leftClick, boolean shiftClick, - boolean rightClick, BlockKey clickedBlock) { - if (rawSlot < 0 || rawSlot >= SIZE) { - return false; - } + String mineName, boolean leftClick, boolean shiftClick, + boolean rightClick, BlockKey clickedBlock) { + if (rawSlot < 0 || rawSlot >= SIZE) return false; if (rawSlot == SLOT_BACK) { MineEditGui.open(plugin, player, mineName); @@ -95,14 +89,61 @@ public final class BlocksGui { } if (rawSlot < 36 && clickedBlock != null) { - BlockWeightEditor.adjustBlockWeight(plugin, player, mineName, clickedBlock, leftClick, shiftClick, rightClick); + adjustBlockWeight(plugin, player, mineName, clickedBlock, leftClick, shiftClick, rightClick); 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 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) { - return BlockWeightEditor.getBlockAtSlot(inv, slot, plugin, 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> 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; } } diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/view/BlocksGuiItems.java b/src/main/java/dev/loki/lomines/gui/mine/edit/BlocksGuiItems.java similarity index 61% rename from src/main/java/dev/loki/lomines/gui/mine/edit/blocks/view/BlocksGuiItems.java rename to src/main/java/dev/loki/lomines/gui/mine/edit/BlocksGuiItems.java index 164c4b5..01adf2a 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/view/BlocksGuiItems.java +++ b/src/main/java/dev/loki/lomines/gui/mine/edit/BlocksGuiItems.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui.mine.edit.blocks.view; +package dev.loki.lomines.gui.mine.edit; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.gui.common.ItemStackFactory; @@ -10,8 +10,7 @@ import org.bukkit.inventory.ItemStack; */ final class BlocksGuiItems { - private BlocksGuiItems() { - } + private BlocksGuiItems() {} static ItemStack blockItem(BlockKey key, double weight) { Material material = getMaterialForKey(key); @@ -19,40 +18,40 @@ final class BlocksGuiItems { double percent = weight * 100.0; return ItemStackFactory.create(material, "§a§l" + name, - "§8───────────────", - "§7Вес: §f" + String.format("%.1f%%", percent), - "", - "§e▸ ЛКМ §7+5%", - "§e▸ ПКМ §7-5%", - "§e▸ Shift+ЛКМ §7+1%", - "§e▸ Shift+ПКМ §7-1% §8(удалить если 0%)", - "", - "§8ID: §7" + key.serialize() + "§8───────────────", + "§7Вес: §f" + String.format("%.1f%%", percent), + "", + "§e▸ ЛКМ §7+5%", + "§e▸ ПКМ §7-5%", + "§e▸ Shift+ЛКМ §7+1%", + "§e▸ Shift+ПКМ §7-1% §8(удалить если 0%)", + "", + "§8ID: §7" + key.serialize() ); } static ItemStack addBlockItem() { return ItemStackFactory.create(Material.EMERALD_BLOCK, "§a§lДобавить блок", - "§8───────────────", - "§7Добавить новый блок", - "§7в конфигурацию", - "", - "§e▸ Нажмите для выбора материала" + "§8───────────────", + "§7Добавить новый блок", + "§7в конфигурацию", + "", + "§e▸ Нажмите для выбора материала" ); } static ItemStack backItem() { return ItemStackFactory.create(Material.ARROW, "§c§lНазад", - "§8───────────────", - "§7Вернуться в редактор шахты", - "", - "§e▸ Нажмите для возврата" + "§8───────────────", + "§7Вернуться в редактор шахты", + "", + "§e▸ Нажмите для возврата" ); } static Material getMaterialForKey(BlockKey key) { - if (key instanceof BlockKey.Vanilla(Material material)) { - return material; + if (key instanceof BlockKey.Vanilla vanilla) { + return vanilla.material(); } if (key instanceof BlockKey.Oraxen) { return Material.NETHER_STAR; @@ -81,8 +80,8 @@ final class BlocksGuiItems { for (String word : input.split(" ")) { if (!word.isEmpty()) { result.append(Character.toUpperCase(word.charAt(0))) - .append(word.substring(1)) - .append(" "); + .append(word.substring(1)) + .append(" "); } } return result.toString().trim(); diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGui.java b/src/main/java/dev/loki/lomines/gui/mine/edit/ResetGui.java similarity index 77% rename from src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGui.java rename to src/main/java/dev/loki/lomines/gui/mine/edit/ResetGui.java index 2885ff9..fe71ef2 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGui.java +++ b/src/main/java/dev/loki/lomines/gui/mine/edit/ResetGui.java @@ -1,9 +1,9 @@ -package dev.loki.lomines.gui.mine.edit.reset; +package dev.loki.lomines.gui.mine.edit; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.gui.common.ItemStackFactory; -import dev.loki.lomines.gui.mine.holder.edit.reset.ResetGuiHolder; +import dev.loki.lomines.gui.mine.holder.ResetGuiHolder; import dev.loki.lomines.gui.mine.main.MineEditGui; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -24,8 +24,7 @@ public final class ResetGui { private static final int SLOT_MESSAGE = 14; private static final int SLOT_COMMANDS = 16; - private ResetGui() { - } + private ResetGui() {} public static void open(LoMinesPlugin plugin, Player player, String mineName) { Mine mine = plugin.getMines().find(mineName).orElse(null); @@ -65,11 +64,9 @@ public final class ResetGui { } public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, - String mineName, boolean leftClick, boolean rightClick, - boolean shiftClick) { - if (rawSlot < 0 || rawSlot >= SIZE) { - return false; - } + String mineName, boolean leftClick, boolean rightClick, + boolean shiftClick) { + if (rawSlot < 0 || rawSlot >= SIZE) return false; if (rawSlot == SLOT_BACK) { MineEditGui.open(plugin, player, mineName); @@ -87,7 +84,14 @@ public final class ResetGui { } if (rawSlot == SLOT_MESSAGE) { - return handleMessageSlot(plugin, player, mineName, leftClick, rightClick); + if (leftClick) { + player.sendMessage(Component.text("§eВведите сообщение в чат (или 'cancel' для отмены):")); + player.closeInventory(); + } else if (rightClick) { + player.sendMessage(Component.text("§cСообщение очищено.")); + refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin); + } + return true; } if (rawSlot == SLOT_COMMANDS && leftClick) { @@ -98,18 +102,6 @@ public final class ResetGui { return true; } - private static boolean handleMessageSlot(LoMinesPlugin plugin, Player player, String mineName, - boolean leftClick, boolean rightClick) { - if (leftClick) { - player.sendMessage(Component.text("§eВведите сообщение в чат (или 'cancel' для отмены):")); - player.closeInventory(); - } else if (rightClick) { - player.sendMessage(Component.text("§cСообщение очищено.")); - refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin); - } - return true; - } - private static void adjustInterval(LoMinesPlugin plugin, Player player, String mineName, boolean leftClick, boolean rightClick, boolean shiftClick) { int delta = 0; @@ -130,7 +122,7 @@ public final class ResetGui { } private static void togglePercentTrigger(LoMinesPlugin plugin, Player player, - String mineName, boolean leftClick, boolean rightClick) { + String mineName, boolean leftClick, boolean rightClick) { if (leftClick) { player.sendMessage(Component.text("§aТриггер по % переключен.")); } else if (rightClick) { diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/ResetGuiItems.java b/src/main/java/dev/loki/lomines/gui/mine/edit/ResetGuiItems.java new file mode 100644 index 0000000..cb253e8 --- /dev/null +++ b/src/main/java/dev/loki/lomines/gui/mine/edit/ResetGuiItems.java @@ -0,0 +1,82 @@ +package dev.loki.lomines.gui.mine.edit; + +import dev.loki.lomines.data.config.reset.ResetConfig; +import dev.loki.lomines.gui.common.ItemStackFactory; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +/** + * Item factory for ResetGui. + */ +final class ResetGuiItems { + + private ResetGuiItems() {} + + static ItemStack intervalItem(ResetConfig config) { + return ItemStackFactory.create(Material.CLOCK, "§a§lИнтервал сброса", + "§8───────────────", + "§7Текущее: §f" + config.intervalDisplay(), + "", + "§e▸ ЛКМ §7+1 минута", + "§e▸ ПКМ §7-1 минута", + "§e▸ Shift+ЛКМ §7+10 минут", + "§e▸ Shift+ПКМ §7-10 минут" + ); + } + + static ItemStack percentTriggerItem(ResetConfig config) { + String status = config.isPercentTriggerEnabled() ? "§aвкл" : "§7выкл"; + String value = config.isPercentTriggerEnabled() ? config.percentTrigger() + "%" : "—"; + + return ItemStackFactory.create(Material.COMPARATOR, "§a§lТриггер по %", + "§8───────────────", + "§7Статус: " + status, + "§7Значение: §f" + value, + "", + "§7Сброс шахты когда", + "§7добыто указанный %", + "", + "§e▸ ЛКМ §7вкл/выкл", + "§e▸ ПКМ §7изменить %" + ); + } + + static ItemStack messageItem(ResetConfig config) { + String message = config.broadcastMessage().isEmpty() ? "§7(не задано)" : "§f" + config.broadcastMessage(); + return ItemStackFactory.create(Material.BOOK, "§a§lСообщение при сбросе", + "§8───────────────", + "§7Текущее:", + " " + ItemStackFactory.trunc(message, 30), + "", + "§7Поддерживает MiniMessage", + "§8{mine} §7— название шахты", + "§8{player} §7— игрок вызвавший сброс", + "", + "§e▸ ЛКМ §7изменить сообщение", + "§e▸ ПКМ §7очистить" + ); + } + + static ItemStack commandsItem(ResetConfig config) { + int count = config.commands().size(); + return ItemStackFactory.create(Material.COMMAND_BLOCK, "§a§lКоманды при сбросе", + "§8───────────────", + "§7Количество: §f" + count, + "", + "§7Плейсхолдеры:", + "§8{mine} §7— название шахты", + "§8{world} §7— мир", + "", + "§e▸ ЛКМ §7редактировать список" + ); + } + + static ItemStack backItem() { + return ItemStackFactory.create(Material.ARROW, "§c§lНазад", + "§8───────────────", + "§7Вернуться в редактор шахты", + "", + "§e▸ Нажмите для возврата" + ); + } +} diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/rewards/RewardsGui.java b/src/main/java/dev/loki/lomines/gui/mine/edit/RewardsGui.java similarity index 70% rename from src/main/java/dev/loki/lomines/gui/mine/edit/rewards/RewardsGui.java rename to src/main/java/dev/loki/lomines/gui/mine/edit/RewardsGui.java index 9ea948e..feb9679 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/rewards/RewardsGui.java +++ b/src/main/java/dev/loki/lomines/gui/mine/edit/RewardsGui.java @@ -1,11 +1,9 @@ -package dev.loki.lomines.gui.mine.edit.rewards; +package dev.loki.lomines.gui.mine.edit; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.reward.RewardConfig; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.RewardConfig; import dev.loki.lomines.gui.common.ItemStackFactory; -import dev.loki.lomines.gui.mine.holder.edit.rewards.RewardsGuiHolder; -import dev.loki.lomines.gui.mine.main.MineEditGui; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; @@ -27,8 +25,7 @@ public final class RewardsGui { private static final int SLOT_BACK = 49; private static final int SLOT_ADD_REWARD = 52; - private RewardsGui() { - } + private RewardsGui() {} public static void open(LoMinesPlugin plugin, Player player, String mineName) { Mine mine = plugin.getMines().find(mineName).orElse(null); @@ -74,51 +71,49 @@ public final class RewardsGui { int itemsCount = entry.items().size(); int commandsCount = entry.commands().size(); String blocksList = String.join(", ", entry.blocks().stream() - .limit(3) - .map(b -> b.serialize().toLowerCase()) - .toList()); + .limit(3) + .map(b -> b.name().toLowerCase()) + .toList()); if (entry.blocks().size() > 3) { blocksList += "..."; } return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграда #" + index, - "§8───────────────", - "§7Шанс: §f" + String.format("%.1f%%", entry.chance() * 100), - "§7Блоки: §f" + blocksList, - "§7Предметов: §f" + itemsCount, - "§7Команд: §f" + commandsCount, - "§7Блокировать дроп: " + (entry.preventVanillaDrops() ? "§aда" : "§7нет"), - "", - "§e▸ ЛКМ §7редактировать", - "§e▸ ПКМ §7удалить" + "§8───────────────", + "§7Шанс: §f" + String.format("%.1f%%", entry.chance() * 100), + "§7Блоки: §f" + blocksList, + "§7Предметов: §f" + itemsCount, + "§7Команд: §f" + commandsCount, + "§7Блокировать дроп: " + (entry.preventVanillaDrops() ? "§aда" : "§7нет"), + "", + "§e▸ ЛКМ §7редактировать", + "§e▸ ПКМ §7удалить" ); } private static ItemStack addRewardItem() { return ItemStackFactory.create(Material.EMERALD_BLOCK, "§a§lДобавить награду", - "§8───────────────", - "§7Создать новую запись", - "§7награды", - "", - "§e▸ Нажмите для добавления" + "§8───────────────", + "§7Создать новую запись", + "§7награды", + "", + "§e▸ Нажмите для добавления" ); } private static ItemStack backItem() { return ItemStackFactory.create(Material.ARROW, "§c§lНазад", - "§8───────────────", - "§7Вернуться в редактор шахты", - "", - "§e▸ Нажмите для возврата" + "§8───────────────", + "§7Вернуться в редактор шахты", + "", + "§e▸ Нажмите для возврата" ); } public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, - String mineName, boolean leftClick, boolean rightClick, - int rewardIndex) { - if (rawSlot < 0 || rawSlot >= SIZE) { - return false; - } + String mineName, boolean leftClick, boolean rightClick, + int rewardIndex) { + if (rawSlot < 0 || rawSlot >= SIZE) return false; if (rawSlot == SLOT_BACK) { MineEditGui.open(plugin, player, mineName); diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/edit/BlockWeightEditor.java b/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/edit/BlockWeightEditor.java deleted file mode 100644 index c5eec72..0000000 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/edit/BlockWeightEditor.java +++ /dev/null @@ -1,90 +0,0 @@ -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 weights = new java.util.HashMap<>(config.weights()); - double currentWeight = weights.getOrDefault(blockKey, 0.0) * 100.0; - - double newWeight = Math.max(0, Math.min(100, - currentWeight + computeDelta(leftClick, shiftClick, rightClick))); - - applyWeightChange(player, blockKey, weights, newWeight, rightClick, shiftClick); - - BlocksGui.refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin); - } - - private static double computeDelta(boolean leftClick, boolean shiftClick, boolean rightClick) { - if (leftClick && shiftClick) { - return 1.0; - } - if (rightClick && shiftClick) { - return -1.0; - } - if (leftClick) { - return 5.0; - } - if (rightClick) { - return -5.0; - } - return 0; - } - - private static void applyWeightChange(Player player, BlockKey blockKey, Map weights, - double newWeight, boolean rightClick, boolean shiftClick) { - if (rightClick && shiftClick && newWeight <= 0.1) { - weights.remove(blockKey); - player.sendMessage(Component.text( - "\u00a7c\u0411\u043b\u043e\u043a \u00a7f" + blockKey.serialize() + " \u00a7c\u0443\u0434\u0430\u043b\u0435\u043d!")); - } else { - weights.put(blockKey, newWeight / 100.0); - player.sendMessage(Component.text( - "\u00a7a\u0412\u0435\u0441 \u00a7f" + blockKey.serialize() + " \u00a7a\u0438\u0437\u043c\u0435\u043d\u0435\u043d: \u00a7f" - + String.format("%.1f%%", newWeight))); - } - } - - public static BlockKey getBlockAtSlot(Inventory inv, int slot, LoMinesPlugin plugin, String mineName) { - if (slot < 0 || slot >= 36) { - return null; - } - - Mine mine = plugin.getMines().find(mineName).orElse(null); - if (mine == null) { - return null; - } - - BlockConfig config = mine.getConfig().blocks(); - List> 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; - } -} diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/select/BlockMaterialSelectionGui.java b/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/select/BlockMaterialSelectionGui.java deleted file mode 100644 index 5059f74..0000000 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/select/BlockMaterialSelectionGui.java +++ /dev/null @@ -1,106 +0,0 @@ -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 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 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); - } -} diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/select/BlockMaterialSelector.java b/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/select/BlockMaterialSelector.java deleted file mode 100644 index bb52192..0000000 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/blocks/select/BlockMaterialSelector.java +++ /dev/null @@ -1,83 +0,0 @@ -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 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 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"); - } -} diff --git a/src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGuiItems.java b/src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGuiItems.java deleted file mode 100644 index c09aa83..0000000 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGuiItems.java +++ /dev/null @@ -1,83 +0,0 @@ -package dev.loki.lomines.gui.mine.edit.reset; - -import dev.loki.lomines.data.config.reset.ResetConfig; -import dev.loki.lomines.gui.common.ItemStackFactory; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -/** - * Item factory for ResetGui. - */ -final class ResetGuiItems { - - private ResetGuiItems() { - } - - static ItemStack intervalItem(ResetConfig config) { - return ItemStackFactory.create(Material.CLOCK, "§a§lИнтервал сброса", - "§8───────────────", - "§7Текущее: §f" + config.intervalDisplay(), - "", - "§e▸ ЛКМ §7+1 минута", - "§e▸ ПКМ §7-1 минута", - "§e▸ Shift+ЛКМ §7+10 минут", - "§e▸ Shift+ПКМ §7-10 минут" - ); - } - - static ItemStack percentTriggerItem(ResetConfig config) { - String status = config.isPercentTriggerEnabled() ? "§aвкл" : "§7выкл"; - String value = config.isPercentTriggerEnabled() ? config.percentTrigger() + "%" : "—"; - - return ItemStackFactory.create(Material.COMPARATOR, "§a§lТриггер по %", - "§8───────────────", - "§7Статус: " + status, - "§7Значение: §f" + value, - "", - "§7Сброс шахты когда", - "§7добыто указанный %", - "", - "§e▸ ЛКМ §7вкл/выкл", - "§e▸ ПКМ §7изменить %" - ); - } - - static ItemStack messageItem(ResetConfig config) { - String message = config.broadcastMessage().isEmpty() ? "§7(не задано)" : "§f" + config.broadcastMessage(); - return ItemStackFactory.create(Material.BOOK, "§a§lСообщение при сбросе", - "§8───────────────", - "§7Текущее:", - " " + ItemStackFactory.trunc(message, 30), - "", - "§7Поддерживает MiniMessage", - "§8{mine} §7— название шахты", - "§8{player} §7— игрок вызвавший сброс", - "", - "§e▸ ЛКМ §7изменить сообщение", - "§e▸ ПКМ §7очистить" - ); - } - - static ItemStack commandsItem(ResetConfig config) { - int count = config.commands().size(); - return ItemStackFactory.create(Material.COMMAND_BLOCK, "§a§lКоманды при сбросе", - "§8───────────────", - "§7Количество: §f" + count, - "", - "§7Плейсхолдеры:", - "§8{mine} §7— название шахты", - "§8{world} §7— мир", - "", - "§e▸ ЛКМ §7редактировать список" - ); - } - - static ItemStack backItem() { - return ItemStackFactory.create(Material.ARROW, "§c§lНазад", - "§8───────────────", - "§7Вернуться в редактор шахты", - "", - "§e▸ Нажмите для возврата" - ); - } -} diff --git a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/BlockMaterialSelectionGuiHolder.java b/src/main/java/dev/loki/lomines/gui/mine/holder/BlockMaterialSelectionGuiHolder.java similarity index 95% rename from src/main/java/dev/loki/lomines/gui/mine/holder/edit/BlockMaterialSelectionGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/mine/holder/BlockMaterialSelectionGuiHolder.java index e222e1d..b8e1661 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/BlockMaterialSelectionGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/mine/holder/BlockMaterialSelectionGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui.mine.holder.edit; +package dev.loki.lomines.gui.mine.holder; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/BlocksGuiHolder.java b/src/main/java/dev/loki/lomines/gui/mine/holder/BlocksGuiHolder.java similarity index 94% rename from src/main/java/dev/loki/lomines/gui/mine/holder/edit/BlocksGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/mine/holder/BlocksGuiHolder.java index 01875fb..94d66ee 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/BlocksGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/mine/holder/BlocksGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui.mine.holder.edit; +package dev.loki.lomines.gui.mine.holder; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/mine/holder/main/MineEditGuiHolder.java b/src/main/java/dev/loki/lomines/gui/mine/holder/MineEditGuiHolder.java similarity index 94% rename from src/main/java/dev/loki/lomines/gui/mine/holder/main/MineEditGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/mine/holder/MineEditGuiHolder.java index 26e07d8..74cad36 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/holder/main/MineEditGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/mine/holder/MineEditGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui.mine.holder.main; +package dev.loki.lomines.gui.mine.holder; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/reset/ResetGuiHolder.java b/src/main/java/dev/loki/lomines/gui/mine/holder/ResetGuiHolder.java similarity index 93% rename from src/main/java/dev/loki/lomines/gui/mine/holder/edit/reset/ResetGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/mine/holder/ResetGuiHolder.java index 5d33d9a..0bde436 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/reset/ResetGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/mine/holder/ResetGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui.mine.holder.edit.reset; +package dev.loki.lomines.gui.mine.holder; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/rewards/RewardsGuiHolder.java b/src/main/java/dev/loki/lomines/gui/mine/holder/RewardsGuiHolder.java similarity index 93% rename from src/main/java/dev/loki/lomines/gui/mine/holder/edit/rewards/RewardsGuiHolder.java rename to src/main/java/dev/loki/lomines/gui/mine/holder/RewardsGuiHolder.java index 2cb105c..cba3849 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/holder/edit/rewards/RewardsGuiHolder.java +++ b/src/main/java/dev/loki/lomines/gui/mine/holder/RewardsGuiHolder.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.gui.mine.holder.edit.rewards; +package dev.loki.lomines.gui.mine.holder; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; diff --git a/src/main/java/dev/loki/lomines/gui/mine/main/MineEditExtraItems.java b/src/main/java/dev/loki/lomines/gui/mine/main/MineEditExtraItems.java deleted file mode 100644 index 3d56eed..0000000 --- a/src/main/java/dev/loki/lomines/gui/mine/main/MineEditExtraItems.java +++ /dev/null @@ -1,111 +0,0 @@ -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 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"); - lore.add(" §8• §7/lm setspawn §f"); - lore.add(" §8• §7/lm clearspawn §f"); - 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▸ Нажмите чтобы закрыть" - ); - } -} diff --git a/src/main/java/dev/loki/lomines/gui/mine/main/MineEditGui.java b/src/main/java/dev/loki/lomines/gui/mine/main/MineEditGui.java index 60386da..31983aa 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/main/MineEditGui.java +++ b/src/main/java/dev/loki/lomines/gui/mine/main/MineEditGui.java @@ -1,13 +1,10 @@ package dev.loki.lomines.gui.mine.main; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.gui.common.ItemStackFactory; import dev.loki.lomines.gui.confirm.ConfirmDeleteGui; -import dev.loki.lomines.gui.mine.edit.blocks.view.BlocksGui; -import dev.loki.lomines.gui.mine.edit.reset.ResetGui; -import dev.loki.lomines.gui.mine.edit.rewards.RewardsGui; -import dev.loki.lomines.gui.mine.holder.main.MineEditGuiHolder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; @@ -21,6 +18,9 @@ import org.bukkit.inventory.Inventory; */ public final class MineEditGui { + private static final int SIZE = 54; + private static final String TITLE_PREFIX = "Редактор шахты: "; + // Slot constants public static final int SLOT_BACK = 49; public static final int SLOT_BLOCKS = 10; @@ -32,11 +32,8 @@ public final class MineEditGui { public static final int SLOT_SAVE = 32; public static final int SLOT_DELETE = 34; public static final int SLOT_INFO = 40; - private static final int SIZE = 54; - private static final String TITLE_PREFIX = "Редактор шахты: "; - private MineEditGui() { - } + private MineEditGui() {} public static void open(LoMinesPlugin plugin, Player player, String mineName) { Mine mine = plugin.getMines().find(mineName).orElse(null); @@ -45,7 +42,8 @@ public final class MineEditGui { return; } MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName); - Inventory inv = Bukkit.createInventory(holder, SIZE, Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN)); + Inventory inv = Bukkit.createInventory(holder, SIZE, + Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN)); holder.setInventory(inv); fill(plugin, mine, inv); player.openInventory(inv); @@ -62,30 +60,26 @@ public final class MineEditGui { private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) { 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_REGIONS, MineEditItems.regionsItem(config)); inv.setItem(SLOT_RESET, MineEditItems.resetItem(config)); inv.setItem(SLOT_REWARDS, MineEditItems.rewardsItem(config)); - inv.setItem(SLOT_TELEPORT, MineEditExtraItems.teleportItem(config)); - inv.setItem(SLOT_UI, MineEditExtraItems.uiItem(config)); - inv.setItem(SLOT_SAVE, MineEditExtraItems.saveItem()); - inv.setItem(SLOT_DELETE, MineEditExtraItems.deleteItem(mine.getName())); - inv.setItem(SLOT_INFO, MineEditExtraItems.infoItem(mine)); - inv.setItem(SLOT_BACK, MineEditExtraItems.backItem()); + inv.setItem(SLOT_TELEPORT, MineEditItems.teleportItem(config)); + inv.setItem(SLOT_UI, MineEditItems.uiItem(config)); + inv.setItem(SLOT_SAVE, MineEditItems.saveItem()); + inv.setItem(SLOT_DELETE, MineEditItems.deleteItem(mine.getName())); + inv.setItem(SLOT_INFO, MineEditItems.infoItem(mine)); + inv.setItem(SLOT_BACK, MineEditItems.backItem()); } public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) { - if (rawSlot < 0 || rawSlot >= SIZE) { - return false; - } - return handleNavigationSlot(plugin, player, mineName, rawSlot); - } - - private static boolean handleNavigationSlot(LoMinesPlugin plugin, Player player, String mineName, int rawSlot) { + if (rawSlot < 0 || rawSlot >= SIZE) return false; switch (rawSlot) { + case SLOT_BACK -> { + player.closeInventory(); + return true; + } case SLOT_BLOCKS -> { BlocksGui.open(plugin, player, mineName); return true; @@ -98,22 +92,6 @@ public final class MineEditGui { RewardsGui.open(plugin, player, mineName); return true; } - case SLOT_DELETE -> { - ConfirmDeleteGui.open(plugin, player, mineName); - return true; - } - default -> { - return handleInfoSlot(player, mineName, rawSlot); - } - } - } - - private static boolean handleInfoSlot(Player player, String mineName, int rawSlot) { - switch (rawSlot) { - case SLOT_BACK -> { - player.closeInventory(); - return true; - } case SLOT_TELEPORT -> { player.sendMessage(Component.text("Используйте команды /lm setteleport и /lm setspawn", NamedTextColor.YELLOW)); return true; @@ -127,11 +105,13 @@ public final class MineEditGui { player.closeInventory(); return true; } + case SLOT_DELETE -> { + ConfirmDeleteGui.open(plugin, player, mineName); + return true; + } case SLOT_INFO, SLOT_REGIONS -> { return true; } - default -> {} } return true; } -} \ No newline at end of file diff --git a/src/main/java/dev/loki/lomines/gui/mine/main/MineEditItems.java b/src/main/java/dev/loki/lomines/gui/mine/main/MineEditItems.java index d04849d..2136881 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/main/MineEditItems.java +++ b/src/main/java/dev/loki/lomines/gui/mine/main/MineEditItems.java @@ -1,7 +1,8 @@ package dev.loki.lomines.gui.mine.main; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.data.config.reward.RewardConfig; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.data.config.RewardConfig; import dev.loki.lomines.gui.common.ItemStackFactory; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -16,29 +17,29 @@ final class MineEditItems { private static final String SEP = "§8───────────────"; 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() { return ItemStackFactory.filler(); } static ItemStack blocksItem(MineConfig config) { + var w = config.blocks().weights(); List lore = new ArrayList<>(); lore.add(SEP); lore.add("§7Настройки:"); lore.add(" §fТипов: §7" + config.blocks().blockCount()); lore.add(" §fРежим: §7" + config.blocks().fillMode().name()); lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет")); - var w = config.blocks().weights(); if (!w.isEmpty()) { lore.add(""); lore.add("§7Топ блоков:"); w.entrySet().stream().sorted((a, b) -> Double.compare(b.getValue(), a.getValue())).limit(3) - .forEach(e -> lore.add(String.format(" §8• §7%s §8(§7%.0f%%§8)", e.getKey().serialize(), e.getValue() * 100))); + .forEach(e -> lore.add(String.format(" §8• §7%s §8(§7%.0f%%§8)", e.getKey().serialize(), e.getValue() * 100))); } lore.add(""); lore.add(EDIT); @@ -47,14 +48,14 @@ final class MineEditItems { static ItemStack regionsItem(MineConfig config) { return ItemStackFactory.create(Material.COMPASS, "§a§lРегионы", - SEP, - "§7Настройки:", - " §fКоличество: §7" + config.region().regionCount(), - " §fОбщий объём: §7" + config.region().totalVolume() + " §8блоков", - "", - " §fМир: §7" + config.worldName(), - "", - "§8Изменяется через палочку" + SEP, + "§7Настройки:", + " §fКоличество: §7" + config.region().regionCount(), + " §fОбщий объём: §7" + config.region().totalVolume() + " §8блоков", + "", + " §fМир: §7" + config.worldName(), + "", + "§8Изменяется через палочку" ); } @@ -76,10 +77,10 @@ final class MineEditItems { } static ItemStack rewardsItem(MineConfig config) { - final var entries = config.rewards().entries(); - final long items = entries.stream().flatMap(e -> e.items().stream()).count(); - final long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count(); - final long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count(); + var entries = config.rewards().entries(); + long items = entries.stream().flatMap(e -> e.items().stream()).count(); + long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count(); + long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count(); List lore = new ArrayList<>(); lore.add(SEP); lore.add("§7Настройки:"); @@ -89,13 +90,100 @@ final class MineEditItems { lore.add("§7Типы наград:"); lore.add(" §8• §fПредметов: §7" + items); 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(EDIT); return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0])); } + static ItemStack teleportItem(MineConfig config) { + List 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"); + lore.add(" §8• §7/lm setspawn §f"); + lore.add(" §8• §7/lm clearspawn §f"); + 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 + ); + } } diff --git a/src/main/java/dev/loki/lomines/handler/ui/ActionBarHandler.java b/src/main/java/dev/loki/lomines/handler/ActionBarHandler.java similarity index 87% rename from src/main/java/dev/loki/lomines/handler/ui/ActionBarHandler.java rename to src/main/java/dev/loki/lomines/handler/ActionBarHandler.java index f7d8885..61ebb88 100644 --- a/src/main/java/dev/loki/lomines/handler/ui/ActionBarHandler.java +++ b/src/main/java/dev/loki/lomines/handler/ActionBarHandler.java @@ -1,8 +1,9 @@ -package dev.loki.lomines.handler.ui; - -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.util.location.geo.Cuboid; +package dev.loki.lomines.handler; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.util.format.TimeFormatter; +import dev.loki.lomines.util.location.Cuboid; +import dev.lolib.utils.ActionBar; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -44,7 +45,7 @@ public final class ActionBarHandler { center.getWorld().getNearbyEntities(center, range, range, range).stream() .filter(entity -> entity instanceof Player) .map(entity -> (Player) entity) - .forEach(player -> player.sendActionBar(message)); + .forEach(player -> ActionBar.send(player, message)); } /** @@ -73,9 +74,7 @@ public final class ActionBarHandler { */ private String formatTime() { 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); } } diff --git a/src/main/java/dev/loki/lomines/handler/block/MineBlockHandler.java b/src/main/java/dev/loki/lomines/handler/MineBlockHandler.java similarity index 89% rename from src/main/java/dev/loki/lomines/handler/block/MineBlockHandler.java rename to src/main/java/dev/loki/lomines/handler/MineBlockHandler.java index d5f42b3..b643ae5 100644 --- a/src/main/java/dev/loki/lomines/handler/block/MineBlockHandler.java +++ b/src/main/java/dev/loki/lomines/handler/MineBlockHandler.java @@ -1,9 +1,8 @@ -package dev.loki.lomines.handler.block; +package dev.loki.lomines.handler; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.handler.reward.MineRewardHandler; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.MineConfig; import org.bukkit.block.Block; import org.bukkit.entity.Player; diff --git a/src/main/java/dev/loki/lomines/handler/reset/MineResetHandler.java b/src/main/java/dev/loki/lomines/handler/MineResetHandler.java similarity index 50% rename from src/main/java/dev/loki/lomines/handler/reset/MineResetHandler.java rename to src/main/java/dev/loki/lomines/handler/MineResetHandler.java index 5d3ea3a..ca7c6c4 100644 --- a/src/main/java/dev/loki/lomines/handler/reset/MineResetHandler.java +++ b/src/main/java/dev/loki/lomines/handler/MineResetHandler.java @@ -1,84 +1,118 @@ -package dev.loki.lomines.handler.reset; +package dev.loki.lomines.handler; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.data.config.block.FillMode; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.block.BlockUpdateUtil; +import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.LocationParser; -import org.bukkit.Bukkit; - +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; -/** Thread-safe mine reset with async fill and post-reset actions. */ +/** + * Handles mine reset operations. + * Ensures thread-safe reset execution and prevents concurrent resets. + * + *

Reset process:

+ *
    + *
  1. Check if reset is already running (atomic check)
  2. + *
  3. Fill all regions asynchronously via BlockSetter
  4. + *
  5. Wait for all regions to complete
  6. + *
  7. Execute post-reset actions in main thread
  8. + *
+ * + *

Updated for new configuration system (v2).

+ */ 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); } + /** + * 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.getLogger().severe("Failed to reset mine " + mine.getName() + ": " + e.getMessage()); + 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 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()); + 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 maskPositions = getMaskPositions(); if (maskPositions.isEmpty()) { - plugin.getLogger().warning("No mask positions found for mine " + mine.getName()); + plugin.loLogger().warn("No mask positions found for mine " + mine.getName()); running.set(false); return; } mine.getBlockSetter().fillAtLocations(maskPositions, placed -> - Bukkit.getScheduler().runTask(plugin, () -> onResetComplete(placed, silent))); + 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()) { - Bukkit.getScheduler().runTask(plugin, () -> onResetComplete(totalBlocks.get(), silent)); + Scheduler.get(plugin).run(() -> { + onResetComplete(totalBlocks.get(), silent); + }); } }); } } + /** + * Gets mask positions from config. + */ private List getMaskPositions() { List 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); @@ -86,25 +120,36 @@ public final class MineResetHandler { positions.add(loc); } } catch (Exception e) { - plugin.getLogger().warning("Invalid mask position in mine " + mine.getName() + ": " + posStr); + 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()) { - teleportHandler.teleportStuckPlayers(); + teleportPlayers(); } + running.set(false); } + /** + * Executes commands configured for reset. + */ private void executeResetCommands() { List commands = mine.getConfig().reset().commands(); for (String command : commands) { @@ -113,13 +158,41 @@ public final class MineResetHandler { } } + /** + * Broadcasts reset message to players. + */ private void broadcastReset() { String message = mine.getConfig().reset().broadcastMessage(); - if (message == null || message.isEmpty()) { + if (message != null && !message.isEmpty()) { + String formatted = message.replace("%mine%", mine.getName()); + Component component = MiniMessage.miniMessage().deserialize(formatted); + plugin.getServer().broadcast(component); + } + } + + /** + * Teleports players standing inside the mine to a safe location. + * Uses player-spawn location if configured, otherwise uses teleport location. + * Prevents players from suffocating in blocks by finding a safe teleport spot. + */ + 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; } - String formatted = message.replace("%mine%", mine.getName()); - Component component = MiniMessage.miniMessage().deserialize(formatted); - plugin.getServer().broadcast(component); + 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()) { + if (mine.contains(p.getLocation())) { + p.teleport(safeDest); + } + } } } diff --git a/src/main/java/dev/loki/lomines/handler/reward/MineRewardHandler.java b/src/main/java/dev/loki/lomines/handler/MineRewardHandler.java similarity index 95% rename from src/main/java/dev/loki/lomines/handler/reward/MineRewardHandler.java rename to src/main/java/dev/loki/lomines/handler/MineRewardHandler.java index 1601a85..7310ce9 100644 --- a/src/main/java/dev/loki/lomines/handler/reward/MineRewardHandler.java +++ b/src/main/java/dev/loki/lomines/handler/MineRewardHandler.java @@ -1,7 +1,7 @@ -package dev.loki.lomines.handler.reward; +package dev.loki.lomines.handler; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.data.config.MineConfig; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.reward.RewardConfig; import org.bukkit.Material; diff --git a/src/main/java/dev/loki/lomines/handler/reset/PlayerTeleportHandler.java b/src/main/java/dev/loki/lomines/handler/reset/PlayerTeleportHandler.java deleted file mode 100644 index 5ff08af..0000000 --- a/src/main/java/dev/loki/lomines/handler/reset/PlayerTeleportHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -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(); - } -} diff --git a/src/main/java/dev/loki/lomines/integration/IntegrationManager.java b/src/main/java/dev/loki/lomines/integration/IntegrationManager.java index 43bd620..98e3003 100644 --- a/src/main/java/dev/loki/lomines/integration/IntegrationManager.java +++ b/src/main/java/dev/loki/lomines/integration/IntegrationManager.java @@ -33,13 +33,13 @@ public final class IntegrationManager { try { placeholderExpansion = new LoMinesPlaceholderExpansion(plugin); if (placeholderExpansion.register()) { - plugin.getLogger().info("PlaceholderAPI integration enabled"); - plugin.getLogger().info("Available placeholders: %lomines_mine__percent%, %lomines_player_blocksmined%, etc."); + plugin.loLogger().info("PlaceholderAPI integration enabled"); + plugin.loLogger().info("Available placeholders: %lomines_mine__percent%, %lomines_player_blocksmined%, etc."); } else { - plugin.getLogger().warning("Failed to register PlaceholderAPI expansion"); + plugin.loLogger().warn("Failed to register PlaceholderAPI expansion"); } } catch (Exception e) { - plugin.getLogger().warning("Failed to enable PlaceholderAPI integration: " + e.getMessage()); + plugin.loLogger().warn("Failed to enable PlaceholderAPI integration: " + e.getMessage()); e.printStackTrace(); } } @@ -47,7 +47,7 @@ public final class IntegrationManager { private void checkWorldGuard() { if (isPluginEnabled("WorldGuard")) { - plugin.getLogger().info("WorldGuard detected - auto-region creation enabled"); + plugin.loLogger().info("WorldGuard detected - auto-region creation enabled"); } } @@ -67,7 +67,7 @@ public final class IntegrationManager { try { placeholderExpansion.unregister(); } catch (Exception e) { - plugin.getLogger().warning("Failed to unregister PlaceholderAPI expansion: " + e.getMessage()); + plugin.loLogger().warn("Failed to unregister PlaceholderAPI expansion: " + e.getMessage()); } } } diff --git a/src/main/java/dev/loki/lomines/integration/hologram/HologramManager.java b/src/main/java/dev/loki/lomines/integration/hologram/HologramManager.java index 2d42f8e..ed7ea71 100644 --- a/src/main/java/dev/loki/lomines/integration/hologram/HologramManager.java +++ b/src/main/java/dev/loki/lomines/integration/hologram/HologramManager.java @@ -1,27 +1,28 @@ package dev.loki.lomines.integration.hologram; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.data.config.ui.HologramConfig; import dev.loki.lomines.integration.hologram.provider.DecentHologramsProvider; import dev.loki.lomines.integration.hologram.provider.HolographicDisplaysProvider; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Location; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +/** + * Manages holograms for all mines. + * Displays mine information above each mine region. + */ public final class HologramManager { private final LoMinesPlugin plugin; - private final HologramRenderer renderer; - private final Map activeTasks = new ConcurrentHashMap<>(); private HologramProvider provider; + private final Map activeTasks = new ConcurrentHashMap<>(); public HologramManager(LoMinesPlugin plugin) { this.plugin = plugin; - this.renderer = new HologramRenderer(); initializeProvider(); } @@ -30,49 +31,48 @@ public final class HologramManager { HolographicDisplaysProvider hdProvider = new HolographicDisplaysProvider(); if (hdProvider.isAvailable()) { this.provider = hdProvider; - plugin.getLogger().info("Using HolographicDisplays for holograms"); + plugin.loLogger().info("Using HolographicDisplays for holograms"); return; } DecentHologramsProvider dhProvider = new DecentHologramsProvider(); if (dhProvider.isAvailable()) { this.provider = dhProvider; - plugin.getLogger().info("Using DecentHolograms for holograms"); + plugin.loLogger().info("Using DecentHolograms for holograms"); return; } - plugin.getLogger().warning("No hologram provider found. Install HolographicDisplays or DecentHolograms."); + plugin.loLogger().warn("No hologram provider found. Install HolographicDisplays or DecentHolograms."); } /** * Creates a hologram for a mine if enabled in config. */ public void createMineHologram(Mine mine) { - if (provider == null || !provider.isAvailable()) { - return; - } + if (provider == null || !provider.isAvailable()) return; + HologramConfig config = mine.getConfig().ui().hologram(); - if (!config.enabled()) { - return; - } + if (!config.enabled()) return; + String id = "lomines_" + mine.getName(); - Location loc = renderer.calculateLocation(mine, config.height()); - List lines = renderer.formatLines(config.format(), mine); + Location loc = calculateHologramLocation(mine, config.height()); + List lines = formatLines(config.format(), mine); provider.createHologram(id, loc, lines); startUpdateTask(mine); } + /** + * Updates a mine's hologram with current data. + */ public void updateMineHologram(Mine mine) { - if (provider == null || !provider.isAvailable()) { - return; - } + if (provider == null || !provider.isAvailable()) return; + String id = "lomines_" + mine.getName(); - if (!provider.exists(id)) { - return; - } + if (!provider.exists(id)) return; + HologramConfig config = mine.getConfig().ui().hologram(); - List lines = renderer.formatLines(config.format(), mine); + List lines = formatLines(config.format(), mine); provider.updateHologram(id, lines); } @@ -80,13 +80,12 @@ public final class HologramManager { * Deletes a mine's hologram. */ public void deleteMineHologram(String mineName) { - if (provider == null) { - return; - } + if (provider == null) return; + String id = "lomines_" + mineName; provider.deleteHologram(id); - Integer taskId = activeTasks.remove(mineName); + UUID taskId = activeTasks.remove(mineName); if (taskId != null) { plugin.getServer().getScheduler().cancelTask(taskId); } @@ -96,9 +95,7 @@ public final class HologramManager { * Toggles hologram for a 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(); boolean currentlyEnabled = provider.exists(id); @@ -116,22 +113,79 @@ public final class HologramManager { String mineName = mine.getName(); // Cancel existing task if any - Integer existingTask = activeTasks.remove(mineName); + UUID existingTask = activeTasks.remove(mineName); if (existingTask != null) { plugin.getServer().getScheduler().cancelTask(existingTask); } // Start new update task (every second) int taskId = plugin.getServer().getScheduler().scheduleSyncRepeatingTask( - plugin, - () -> updateMineHologram(mine), - 20L, // Initial delay (1 second) - 20L // Period (1 second) + plugin, + () -> updateMineHologram(mine), + 20L, // Initial delay (1 second) + 20L // Period (1 second) ); - activeTasks.put(mineName, taskId); + activeTasks.put(mineName, new UUID(0, 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 formatLines(List format, Mine mine) { + List result = new ArrayList<>(); + MiniMessage miniMessage = MiniMessage.miniMessage(); + + 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(formatted); + } + + return result; + } + + private String createProgressBar(double percent) { + int filled = (int) Math.round(percent / 10.0); + int empty = 10 - filled; + + StringBuilder bar = new StringBuilder(); + bar.append(""); + bar.append("█".repeat(Math.max(0, filled))); + bar.append(""); + bar.append("░".repeat(Math.max(0, empty))); + bar.append(""); + + return bar.toString(); + } + + 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() { for (String mineName : new ArrayList<>(activeTasks.keySet())) { deleteMineHologram(mineName); diff --git a/src/main/java/dev/loki/lomines/integration/hologram/HologramProvider.java b/src/main/java/dev/loki/lomines/integration/hologram/HologramProvider.java index fbbfc46..a2fed97 100644 --- a/src/main/java/dev/loki/lomines/integration/hologram/HologramProvider.java +++ b/src/main/java/dev/loki/lomines/integration/hologram/HologramProvider.java @@ -13,9 +13,9 @@ public interface HologramProvider { /** * Creates a new hologram at the specified location. * - * @param id unique identifier for this hologram + * @param id unique identifier for this hologram * @param location where to place the hologram - * @param lines initial lines to display + * @param lines initial lines to display * @return true if hologram was created successfully */ boolean createHologram(String id, Location location, List lines); @@ -23,7 +23,7 @@ public interface HologramProvider { /** * Updates the lines of an existing hologram. * - * @param id the hologram identifier + * @param id the hologram identifier * @param lines new lines to display * @return true if updated successfully */ @@ -32,7 +32,7 @@ public interface HologramProvider { /** * Moves a hologram to a new location. * - * @param id the hologram identifier + * @param id the hologram identifier * @param newLocation new location * @return true if moved successfully */ diff --git a/src/main/java/dev/loki/lomines/integration/hologram/HologramRenderer.java b/src/main/java/dev/loki/lomines/integration/hologram/HologramRenderer.java deleted file mode 100644 index fc0225d..0000000 --- a/src/main/java/dev/loki/lomines/integration/hologram/HologramRenderer.java +++ /dev/null @@ -1,53 +0,0 @@ -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 formatLines(List format, Mine mine) { - List 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 "�FF00" + "█".repeat(Math.max(0, filled)) - + "󅒐" + "░".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); - } -} diff --git a/src/main/java/dev/loki/lomines/integration/hologram/provider/DecentHologramsProvider.java b/src/main/java/dev/loki/lomines/integration/hologram/provider/DecentHologramsProvider.java index 619541a..d06883a 100644 --- a/src/main/java/dev/loki/lomines/integration/hologram/provider/DecentHologramsProvider.java +++ b/src/main/java/dev/loki/lomines/integration/hologram/provider/DecentHologramsProvider.java @@ -1,6 +1,7 @@ package dev.loki.lomines.integration.hologram.provider; -import dev.loki.lomines.integration.hologram.HologramProvider; +import eu.decentsoftware.holograms.api.DHAPI; +import eu.decentsoftware.holograms.api.holograms.Hologram; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.plugin.Plugin; @@ -14,7 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; */ public final class DecentHologramsProvider implements HologramProvider { - private final Map holograms = new ConcurrentHashMap<>(); + private final Map holograms = new ConcurrentHashMap<>(); private final boolean available; public DecentHologramsProvider() { @@ -28,54 +29,67 @@ public final class DecentHologramsProvider implements HologramProvider { @Override public boolean createHologram(String id, Location location, List lines) { - if (!available) { - return false; - } - if (holograms.containsKey(id)) { - return false; - } + if (!available) return false; + if (holograms.containsKey(id)) return false; - holograms.put(id, location); - return true; + try { + Hologram hologram = DHAPI.createHologram(id, location, lines); + holograms.put(id, hologram); + return true; + } catch (Exception e) { + return false; + } } @Override public boolean updateHologram(String id, List lines) { - if (!available) { + if (!available) return false; + + Hologram hologram = holograms.get(id); + if (hologram == null) return false; + + try { + DHAPI.setHologramLines(hologram, lines); + return true; + } catch (Exception e) { return false; } - - return holograms.containsKey(id); } @Override public boolean moveHologram(String id, Location newLocation) { - if (!available) { - return false; - } + if (!available) return false; - if (!holograms.containsKey(id)) { + Hologram hologram = holograms.get(id); + if (hologram == null) return false; + + try { + hologram.setLocation(newLocation); + return true; + } catch (Exception e) { return false; } - holograms.put(id, newLocation); - return true; } @Override public boolean deleteHologram(String id) { - if (!available) { + if (!available) return false; + + Hologram hologram = holograms.remove(id); + if (hologram == null) return false; + + try { + hologram.destroy(); + return true; + } catch (Exception e) { return false; } - - return holograms.remove(id) != null; } @Override public boolean exists(String id) { - if (!available) { - return false; - } - return holograms.containsKey(id); + if (!available) return false; + return DHAPI.getHologram(id) != null; } @Override diff --git a/src/main/java/dev/loki/lomines/integration/hologram/provider/HolographicDisplaysProvider.java b/src/main/java/dev/loki/lomines/integration/hologram/provider/HolographicDisplaysProvider.java index 5a14af5..8e628ed 100644 --- a/src/main/java/dev/loki/lomines/integration/hologram/provider/HolographicDisplaysProvider.java +++ b/src/main/java/dev/loki/lomines/integration/hologram/provider/HolographicDisplaysProvider.java @@ -1,6 +1,8 @@ package dev.loki.lomines.integration.hologram.provider; -import dev.loki.lomines.integration.hologram.HologramProvider; +import com.gmail.filoghost.holographicdisplays.api.Hologram; +import com.gmail.filoghost.holographicdisplays.api.HologramsAPI; +import dev.loki.lomines.LoMinesPlugin; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.plugin.Plugin; @@ -14,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap; */ public final class HolographicDisplaysProvider implements HologramProvider { - private final Map holograms = new ConcurrentHashMap<>(); + private final Map holograms = new ConcurrentHashMap<>(); private final boolean available; public HolographicDisplaysProvider() { @@ -28,56 +30,78 @@ public final class HolographicDisplaysProvider implements HologramProvider { @Override public boolean createHologram(String id, Location location, List lines) { - if (!available) { + if (!available) return false; + if (holograms.containsKey(id)) return false; + + try { + Plugin hdPlugin = Bukkit.getPluginManager().getPlugin("HolographicDisplays"); + if (hdPlugin == null) return false; + + Hologram hologram = HologramsAPI.createHologram(hdPlugin, location); + for (String line : lines) { + hologram.appendTextLine(line); + } + holograms.put(id, hologram); + return true; + } catch (Exception e) { return false; } - if (holograms.containsKey(id)) { - return false; - } - // Runtime bridge disabled when API jar is absent at compile time. - // We keep state so manager logic stays consistent. - holograms.put(id, location); - return true; } @Override public boolean updateHologram(String id, List lines) { - if (!available) { + if (!available) return false; + + Hologram hologram = holograms.get(id); + if (hologram == null || hologram.isDeleted()) return false; + + try { + hologram.clearLines(); + for (String line : lines) { + hologram.appendTextLine(line); + } + return true; + } catch (Exception e) { return false; } - - return holograms.containsKey(id); } @Override public boolean moveHologram(String id, Location newLocation) { - if (!available) { - return false; - } + if (!available) return false; - if (!holograms.containsKey(id)) { + Hologram hologram = holograms.get(id); + if (hologram == null || hologram.isDeleted()) return false; + + try { + hologram.teleport(newLocation); + return true; + } catch (Exception e) { return false; } - holograms.put(id, newLocation); - return true; } @Override public boolean deleteHologram(String id) { - if (!available) { + if (!available) return false; + + Hologram hologram = holograms.remove(id); + if (hologram == null) return false; + + try { + hologram.delete(); + return true; + } catch (Exception e) { return false; } - - return holograms.remove(id) != null; } @Override public boolean exists(String id) { - if (!available) { - return false; - } + if (!available) return false; - return holograms.containsKey(id); + Hologram hologram = holograms.get(id); + return hologram != null && !hologram.isDeleted(); } @Override diff --git a/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java b/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java index 1356c94..7d5b98d 100644 --- a/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java +++ b/src/main/java/dev/loki/lomines/integration/placeholder/LoMinesPlaceholderExpansion.java @@ -1,7 +1,7 @@ package dev.loki.lomines.integration.placeholder; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.Mine; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -108,19 +108,19 @@ public final class LoMinesPlaceholderExpansion extends PlaceholderExpansion { String attribute = parts[1]; var statsManager = plugin.getStatsManager(); - var stats = statsManager.getOrCreate(player.getUniqueId()); + var stats = statsManager.getStats(player.getUniqueId()); return switch (attribute) { - case "blocksmined" -> String.valueOf(stats.getTotalBlocks()); - case "minesreset" -> "0"; - case "playtime" -> formatPlayTime(0); - case "rank" -> String.valueOf(statsManager.getLeaderboard().getPosition(player.getUniqueId())); + case "blocksmined" -> String.valueOf(stats.getTotalBlocksMined()); + case "minesreset" -> String.valueOf(stats.getMinesReset()); + case "playtime" -> formatPlayTime(stats.getPlayTimeMinutes()); + case "rank" -> String.valueOf(statsManager.getLeaderboard().getRank(player.getUniqueId())); default -> null; }; } private String formatResetTime(Mine mine) { - int ticks = mine.getTicks(); + int ticks = mine.getTicksSinceReset(); int seconds = ticks / 20; int minutes = seconds / 60; int remainingSeconds = seconds % 60; @@ -128,7 +128,7 @@ public final class LoMinesPlaceholderExpansion extends PlaceholderExpansion { } private int getResetSeconds(Mine mine) { - return mine.getTicks() / 20; + return mine.getTicksSinceReset() / 20; } private String formatPlayTime(int minutes) { diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardConfig.java b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardConfig.java new file mode 100644 index 0000000..6a96631 --- /dev/null +++ b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardConfig.java @@ -0,0 +1,155 @@ +package dev.loki.lomines.integration.worldguard; + +import java.util.List; +import java.util.Objects; +import java.util.Random; + +/** + * Configuration for WorldGuard auto-region creation. + */ +public record WorldGuardConfig( + boolean enabled, + String regionNameTemplate, + List owners, + List members, + List flags, + boolean protectOnCreate +) { + + private static final Random RANDOM = new Random(); + private static final String DEFAULT_TEMPLATE = "{mine_name}_{random_4}"; + + public WorldGuardConfig { + enabled = enabled; + regionNameTemplate = regionNameTemplate != null && !regionNameTemplate.isBlank() + ? regionNameTemplate + : DEFAULT_TEMPLATE; + owners = owners != null ? List.copyOf(owners) : List.of(); + members = members != null ? List.copyOf(members) : List.of(); + flags = flags != null ? List.copyOf(flags) : List.of( + "passthrough=deny", + "build=allow" + ); + protectOnCreate = protectOnCreate; + } + + /** + * Generates a region name from the template. + * + * @param mineName the name of the mine + * @return the generated region name + */ + public String generateRegionName(String mineName) { + String result = regionNameTemplate; + + // 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(); + } + + /** + * Default configuration with auto-region enabled. + */ + public static WorldGuardConfig defaults() { + return new WorldGuardConfig( + true, + DEFAULT_TEMPLATE, + List.of(), + List.of(), + List.of("passthrough=deny", "build=allow"), + true + ); + } + + /** + * Disabled configuration. + */ + public static WorldGuardConfig disabled() { + return new WorldGuardConfig( + false, + DEFAULT_TEMPLATE, + List.of(), + List.of(), + List.of(), + false + ); + } + + /** + * Builder for fluent construction. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private boolean enabled = true; + private String template = DEFAULT_TEMPLATE; + private List owners = List.of(); + private List members = List.of(); + private List flags = List.of("passthrough=deny", "build=allow"); + private boolean protectOnCreate = true; + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder template(String template) { + this.template = template; + return this; + } + + public Builder owners(List owners) { + this.owners = owners; + return this; + } + + public Builder members(List members) { + this.members = members; + return this; + } + + public Builder flags(List flags) { + this.flags = flags; + return this; + } + + public Builder protectOnCreate(boolean protect) { + this.protectOnCreate = protect; + return this; + } + + public WorldGuardConfig build() { + return new WorldGuardConfig(enabled, template, owners, members, flags, protectOnCreate); + } + } +} diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/flag/WorldGuardFlagParser.java b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardFlagParser.java similarity index 85% rename from src/main/java/dev/loki/lomines/integration/worldguard/flag/WorldGuardFlagParser.java rename to src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardFlagParser.java index 798f277..bb722d4 100644 --- a/src/main/java/dev/loki/lomines/integration/worldguard/flag/WorldGuardFlagParser.java +++ b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardFlagParser.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.integration.worldguard.flag; +package dev.loki.lomines.integration.worldguard; import com.sk89q.worldguard.protection.flags.Flags; import com.sk89q.worldguard.protection.flags.StateFlag; @@ -19,7 +19,7 @@ public final class WorldGuardFlagParser { /** * Parses and sets a flag from a "key=value" string entry. * - * @param region the region to set the flag on + * @param region the region to set the flag on * @param flagEntry the flag entry in format "flag=value" */ public void parseAndSetFlag(ProtectedRegion region, String flagEntry) { @@ -40,10 +40,10 @@ public final class WorldGuardFlagParser { case "creeper-explosion" -> setStateFlag(region, Flags.CREEPER_EXPLOSION, value); case "greeting" -> region.setFlag(Flags.GREET_MESSAGE, value); case "farewell" -> region.setFlag(Flags.FAREWELL_MESSAGE, value); - default -> plugin.getLogger().warning("Unknown WorldGuard flag: " + flagName); + default -> plugin.loLogger().warn("Unknown WorldGuard flag: " + flagName); } } catch (Exception e) { - plugin.getLogger().warning("Failed to set flag " + flagName + ": " + e.getMessage()); + plugin.loLogger().warn("Failed to set flag " + flagName + ": " + e.getMessage()); } } diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardMemberHandler.java b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardMemberHandler.java similarity index 84% rename from src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardMemberHandler.java rename to src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardMemberHandler.java index abd6fe4..a651b0b 100644 --- a/src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardMemberHandler.java +++ b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardMemberHandler.java @@ -1,8 +1,7 @@ -package dev.loki.lomines.integration.worldguard.region; +package dev.loki.lomines.integration.worldguard; import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig; import java.util.UUID; @@ -29,7 +28,7 @@ final class WorldGuardMemberHandler { UUID uuid = UUID.fromString(owner.substring(5)); region.getOwners().addPlayer(uuid); } catch (IllegalArgumentException e) { - plugin.getLogger().warning("Invalid owner UUID: " + owner); + plugin.loLogger().warn("Invalid owner UUID: " + owner); } } else { region.getOwners().addPlayer(owner); @@ -44,7 +43,7 @@ final class WorldGuardMemberHandler { UUID uuid = UUID.fromString(member.substring(5)); region.getMembers().addPlayer(uuid); } catch (IllegalArgumentException e) { - plugin.getLogger().warning("Invalid member UUID: " + member); + plugin.loLogger().warn("Invalid member UUID: " + member); } } else { region.getMembers().addPlayer(member); diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardRegionService.java b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardRegionService.java new file mode 100644 index 0000000..ca5972b --- /dev/null +++ b/src/main/java/dev/loki/lomines/integration/worldguard/WorldGuardRegionService.java @@ -0,0 +1,163 @@ +package dev.loki.lomines.integration.worldguard; + +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldguard.WorldGuard; +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.util.location.Cuboid; +import org.bukkit.Bukkit; +import org.bukkit.World; + +import java.util.List; + +/** + * Service for managing WorldGuard regions for mines. + */ +public final class WorldGuardRegionService { + + private final LoMinesPlugin plugin; + private final WorldGuardFlagParser flagParser; + private final WorldGuardMemberHandler memberHandler; + private boolean worldGuardEnabled; + + public WorldGuardRegionService(LoMinesPlugin plugin) { + this.plugin = plugin; + this.flagParser = new WorldGuardFlagParser(plugin); + this.memberHandler = new WorldGuardMemberHandler(plugin); + checkWorldGuard(); + } + + private void checkWorldGuard() { + this.worldGuardEnabled = Bukkit.getPluginManager().getPlugin("WorldGuard") != null + && Bukkit.getPluginManager().isPluginEnabled("WorldGuard"); + + if (worldGuardEnabled) { + plugin.loLogger().info("WorldGuard integration enabled"); + } + } + + public String createRegion(String mineName, MineConfig config) { + if (!worldGuardEnabled) return null; + + WorldGuardConfig wgConfig = config.worldGuard(); + if (wgConfig == null || !wgConfig.enabled()) return null; + + String regionName = wgConfig.generateRegionName(mineName); + + try { + RegionManager regionManager = getRegionManager(config.worldName()); + if (regionManager == null) return null; + + if (regionManager.hasRegion(regionName)) { + plugin.loLogger().info("WorldGuard region already exists: " + regionName); + return regionName; + } + + ProtectedCuboidRegion region = createRegionFromCuboids(regionName, config); + if (region == null) { + plugin.loLogger().warn("Failed to create region for mine " + mineName + ": no valid regions"); + return null; + } + + memberHandler.applyOwnersAndMembers(region, wgConfig); + applyFlags(region, wgConfig.flags()); + + regionManager.addRegion(region); + plugin.loLogger().info("Created WorldGuard region '" + regionName + "' for mine " + mineName); + return regionName; + + } catch (Exception e) { + plugin.loLogger().error("Failed to create WorldGuard region for mine " + mineName + ": " + e.getMessage()); + return null; + } + } + + public boolean updateRegion(String mineName, MineConfig config) { + if (!worldGuardEnabled) return false; + + WorldGuardConfig wgConfig = config.worldGuard(); + if (wgConfig == null || !wgConfig.enabled()) return false; + + String regionName = wgConfig.generateRegionName(mineName); + + try { + RegionManager regionManager = getRegionManager(config.worldName()); + if (regionManager == null) return false; + + if (regionManager.hasRegion(regionName)) { + regionManager.removeRegion(regionName); + } + + return createRegion(mineName, config) != null; + + } catch (Exception e) { + plugin.loLogger().error("Failed to update WorldGuard region: " + e.getMessage()); + return false; + } + } + + public boolean deleteRegion(String mineName, MineConfig config) { + if (!worldGuardEnabled) return false; + + WorldGuardConfig wgConfig = config.worldGuard(); + if (wgConfig == null) return false; + + String regionName = wgConfig.generateRegionName(mineName); + + try { + RegionManager regionManager = getRegionManager(config.worldName()); + if (regionManager == null) return false; + + if (regionManager.hasRegion(regionName)) { + regionManager.removeRegion(regionName); + plugin.loLogger().info("Deleted WorldGuard region: " + regionName); + return true; + } + + return false; + } catch (Exception e) { + plugin.loLogger().error("Failed to delete WorldGuard region: " + e.getMessage()); + return false; + } + } + + private RegionManager getRegionManager(String worldName) { + World world = Bukkit.getWorld(worldName); + if (world == null) return null; + + return WorldGuard.getInstance() + .getPlatform() + .getRegionContainer() + .get(com.sk89q.worldedit.bukkit.BukkitAdapter.adapt(world)); + } + + private ProtectedCuboidRegion createRegionFromCuboids(String regionName, MineConfig config) { + List 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) { + plugin.loLogger().warn("Mine " + regionName + " has multiple regions - " + + "only the first region is fully protected by WorldGuard"); + } + + return mainRegion; + } + + private void applyFlags(ProtectedCuboidRegion region, List flags) { + for (String flagEntry : flags) { + flagParser.parseAndSetFlag(region, flagEntry); + } + } + + public boolean isWorldGuardEnabled() { + return worldGuardEnabled; + } +} diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/config/RegionTemplateConfig.java b/src/main/java/dev/loki/lomines/integration/worldguard/config/RegionTemplateConfig.java deleted file mode 100644 index 1c3f291..0000000 --- a/src/main/java/dev/loki/lomines/integration/worldguard/config/RegionTemplateConfig.java +++ /dev/null @@ -1,50 +0,0 @@ -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); - } -} diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/config/WorldGuardConfig.java b/src/main/java/dev/loki/lomines/integration/worldguard/config/WorldGuardConfig.java deleted file mode 100644 index a2dd18c..0000000 --- a/src/main/java/dev/loki/lomines/integration/worldguard/config/WorldGuardConfig.java +++ /dev/null @@ -1,97 +0,0 @@ -package dev.loki.lomines.integration.worldguard.config; - -import java.util.List; - -public record WorldGuardConfig( - boolean enabled, - String regionNameTemplate, - List owners, - List members, - List flags, - boolean protectOnCreate -) { - - public WorldGuardConfig { - enabled = enabled; - regionNameTemplate = regionNameTemplate != null && !regionNameTemplate.isBlank() - ? regionNameTemplate - : RegionTemplateConfig.DEFAULT_TEMPLATE; - owners = owners != null ? List.copyOf(owners) : List.of(); - members = members != null ? List.copyOf(members) : List.of(); - flags = flags != null && !flags.isEmpty() ? List.copyOf(flags) : List.of("block-break=allow"); - protectOnCreate = protectOnCreate; - } - - public static WorldGuardConfig defaults() { - return new WorldGuardConfig( - true, - RegionTemplateConfig.DEFAULT_TEMPLATE, - List.of(), - List.of(), - List.of("block-break=allow"), - true - ); - } - - public static WorldGuardConfig disabled() { - return new WorldGuardConfig( - false, - RegionTemplateConfig.DEFAULT_TEMPLATE, - List.of(), - List.of(), - List.of(), - false - ); - } - - public static Builder builder() { - return new Builder(); - } - - public String generateRegionName(String mineName) { - return RegionTemplateConfig.resolveTemplate(regionNameTemplate, mineName); - } - - public static class Builder { - private boolean enabled = true; - private String template = RegionTemplateConfig.DEFAULT_TEMPLATE; - private List owners = List.of(); - private List members = List.of(); - private List flags = List.of("block-break=allow"); - private boolean protectOnCreate = true; - - public Builder enabled(boolean enabled) { - this.enabled = enabled; - return this; - } - - public Builder template(String template) { - this.template = template; - return this; - } - - public Builder owners(List owners) { - this.owners = owners; - return this; - } - - public Builder members(List members) { - this.members = members; - return this; - } - - public Builder flags(List flags) { - this.flags = flags; - return this; - } - - public Builder protectOnCreate(boolean protect) { - this.protectOnCreate = protect; - return this; - } - - public WorldGuardConfig build() { - return new WorldGuardConfig(enabled, template, owners, members, flags, protectOnCreate); - } - } -} diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/region/RegionTemplateRenderer.java b/src/main/java/dev/loki/lomines/integration/worldguard/region/RegionTemplateRenderer.java deleted file mode 100644 index 60c59ba..0000000 --- a/src/main/java/dev/loki/lomines/integration/worldguard/region/RegionTemplateRenderer.java +++ /dev/null @@ -1,43 +0,0 @@ -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 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 flags) { - for (String flagEntry : flags) { - flagParser.parseAndSetFlag(region, flagEntry); - } - } -} diff --git a/src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardRegionService.java b/src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardRegionService.java deleted file mode 100644 index 648e76f..0000000 --- a/src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardRegionService.java +++ /dev/null @@ -1,140 +0,0 @@ -package dev.loki.lomines.integration.worldguard.region; - -import com.sk89q.worldguard.WorldGuard; -import com.sk89q.worldguard.protection.managers.RegionManager; -import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig; -import dev.loki.lomines.integration.worldguard.flag.WorldGuardFlagParser; -import org.bukkit.Bukkit; -import org.bukkit.World; - -public final class WorldGuardRegionService { - - private final LoMinesPlugin plugin; - private final WorldGuardFlagParser flagParser; - private final WorldGuardMemberHandler memberHandler; - private final RegionTemplateRenderer templateRenderer; - private boolean worldGuardEnabled; - - public WorldGuardRegionService(LoMinesPlugin plugin) { - this.plugin = plugin; - this.flagParser = new WorldGuardFlagParser(plugin); - this.memberHandler = new WorldGuardMemberHandler(plugin); - this.templateRenderer = new RegionTemplateRenderer(flagParser); - checkWorldGuard(); - } - - private void checkWorldGuard() { - this.worldGuardEnabled = Bukkit.getPluginManager().getPlugin("WorldGuard") != null - && Bukkit.getPluginManager().isPluginEnabled("WorldGuard"); - - if (worldGuardEnabled) { - plugin.getLogger().info("WorldGuard integration enabled"); - } - } - - public String createRegion(String mineName, MineConfig config) { - if (!worldGuardEnabled) { - return null; - } - WorldGuardConfig wgConfig = config.worldGuard(); - if (wgConfig == null || !wgConfig.enabled()) { - return null; - } - String regionName = wgConfig.generateRegionName(mineName); - try { - return createRegionInManager(mineName, config, regionName, wgConfig); - } catch (Exception e) { - plugin.getLogger().severe("Failed to create WorldGuard region for mine " + mineName + ": " + e.getMessage()); - return null; - } - } - - private String createRegionInManager(String mineName, MineConfig config, String regionName, WorldGuardConfig wgConfig) { - RegionManager regionManager = getRegionManager(config.worldName()); - if (regionManager == null) { - return null; - } - if (regionManager.hasRegion(regionName)) { - plugin.getLogger().info("WorldGuard region already exists: " + regionName); - return regionName; - } - ProtectedCuboidRegion region = templateRenderer.createRegionFromCuboids(regionName, config); - if (region == null) { - plugin.getLogger().warning("Failed to create region for mine " + mineName + ": no valid regions"); - return null; - } - memberHandler.applyOwnersAndMembers(region, wgConfig); - templateRenderer.applyFlags(region, wgConfig.flags()); - regionManager.addRegion(region); - plugin.getLogger().info("Created WorldGuard region '" + regionName + "' for mine " + mineName); - return regionName; - } - - public boolean updateRegion(String mineName, MineConfig config) { - if (!worldGuardEnabled) { - return false; - } - WorldGuardConfig wgConfig = config.worldGuard(); - if (wgConfig == null || !wgConfig.enabled()) { - return false; - } - String regionName = wgConfig.generateRegionName(mineName); - try { - RegionManager regionManager = getRegionManager(config.worldName()); - if (regionManager == null) { - return false; - } - if (regionManager.hasRegion(regionName)) { - regionManager.removeRegion(regionName); - } - return createRegion(mineName, config) != null; - } catch (Exception e) { - plugin.getLogger().severe("Failed to update WorldGuard region: " + e.getMessage()); - return false; - } - } - - public boolean deleteRegion(String mineName, MineConfig config) { - if (!worldGuardEnabled) { - return false; - } - WorldGuardConfig wgConfig = config.worldGuard(); - if (wgConfig == null) { - return false; - } - String regionName = wgConfig.generateRegionName(mineName); - try { - RegionManager regionManager = getRegionManager(config.worldName()); - if (regionManager == null) { - return false; - } - if (regionManager.hasRegion(regionName)) { - regionManager.removeRegion(regionName); - plugin.getLogger().info("Deleted WorldGuard region: " + regionName); - return true; - } - return false; - } catch (Exception e) { - plugin.getLogger().severe("Failed to delete WorldGuard region: " + e.getMessage()); - return false; - } - } - - private RegionManager getRegionManager(String worldName) { - World world = Bukkit.getWorld(worldName); - if (world == null) { - return null; - } - return WorldGuard.getInstance() - .getPlatform() - .getRegionContainer() - .get(com.sk89q.worldedit.bukkit.BukkitAdapter.adapt(world)); - } - - public boolean isWorldGuardEnabled() { - return worldGuardEnabled; - } -} diff --git a/src/main/java/dev/loki/lomines/listener/block/BlockBreakListener.java b/src/main/java/dev/loki/lomines/listener/BlockBreakListener.java similarity index 92% rename from src/main/java/dev/loki/lomines/listener/block/BlockBreakListener.java rename to src/main/java/dev/loki/lomines/listener/BlockBreakListener.java index bf8a051..fd3b312 100644 --- a/src/main/java/dev/loki/lomines/listener/block/BlockBreakListener.java +++ b/src/main/java/dev/loki/lomines/listener/BlockBreakListener.java @@ -1,7 +1,7 @@ -package dev.loki.lomines.listener.block; +package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.Mine; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; diff --git a/src/main/java/dev/loki/lomines/listener/gui/GroupGuiListener.java b/src/main/java/dev/loki/lomines/listener/GroupGuiListener.java similarity index 97% rename from src/main/java/dev/loki/lomines/listener/gui/GroupGuiListener.java rename to src/main/java/dev/loki/lomines/listener/GroupGuiListener.java index 1668b3d..ca94f44 100644 --- a/src/main/java/dev/loki/lomines/listener/gui/GroupGuiListener.java +++ b/src/main/java/dev/loki/lomines/listener/GroupGuiListener.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.listener.gui; +package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.gui.group.GroupCreateGui; diff --git a/src/main/java/dev/loki/lomines/listener/MineEditGuiListener.java b/src/main/java/dev/loki/lomines/listener/MineEditGuiListener.java new file mode 100644 index 0000000..5292e47 --- /dev/null +++ b/src/main/java/dev/loki/lomines/listener/MineEditGuiListener.java @@ -0,0 +1,177 @@ +package dev.loki.lomines.listener; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.gui.confirm.ConfirmDeleteGui; +import dev.loki.lomines.gui.confirm.ConfirmDeleteGuiHolder; +import dev.loki.lomines.gui.mine.edit.BlockMaterialSelectionGui; +import dev.loki.lomines.gui.mine.edit.BlocksGui; +import dev.loki.lomines.gui.mine.edit.ResetGui; +import dev.loki.lomines.gui.mine.edit.RewardsGui; +import dev.loki.lomines.gui.mine.holder.BlockMaterialSelectionGuiHolder; +import dev.loki.lomines.gui.mine.holder.BlocksGuiHolder; +import dev.loki.lomines.gui.mine.holder.ResetGuiHolder; +import dev.loki.lomines.gui.mine.holder.RewardsGuiHolder; +import dev.loki.lomines.gui.mine.holder.MineEditGuiHolder; +import dev.loki.lomines.gui.mine.main.MineEditGui; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.InventoryView; + +/** + * Handles clicks in all mine editor GUIs. + * Covers MineEditGui, BlocksGui, ResetGui, RewardsGui, ConfirmDeleteGui, and BlockMaterialSelectionGui. + */ +public final class MineEditGuiListener implements Listener { + + private final LoMinesPlugin plugin; + + public MineEditGuiListener(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + InventoryView view = event.getView(); + + // Handle MineEditGui + if (view.getTopInventory().getHolder() instanceof MineEditGuiHolder holder) { + handleMineEditGui(event, holder); + return; + } + + // Handle BlocksGui + if (view.getTopInventory().getHolder() instanceof BlocksGuiHolder holder) { + handleBlocksGui(event, holder); + return; + } + + // Handle BlockMaterialSelectionGui + if (view.getTopInventory().getHolder() instanceof BlockMaterialSelectionGuiHolder holder) { + handleBlockMaterialSelectionGui(event, holder); + return; + } + + // Handle ResetGui + if (view.getTopInventory().getHolder() instanceof ResetGuiHolder holder) { + handleResetGui(event, holder); + return; + } + + // Handle RewardsGui + if (view.getTopInventory().getHolder() instanceof RewardsGuiHolder holder) { + handleRewardsGui(event, holder); + return; + } + + // Handle ConfirmDeleteGui + if (view.getTopInventory().getHolder() instanceof ConfirmDeleteGuiHolder holder) { + handleConfirmDeleteGui(event, holder); + } + } + + private void handleMineEditGui(InventoryClickEvent event, MineEditGuiHolder holder) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!holder.getPlayerId().equals(player.getUniqueId())) { + event.setCancelled(true); + return; + } + event.setCancelled(true); + int raw = event.getRawSlot(); + if (raw >= event.getView().getTopInventory().getSize()) return; + MineEditGui.handleClick(plugin, player, raw, holder.getMineName()); + } + + private void handleBlocksGui(InventoryClickEvent event, BlocksGuiHolder holder) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!holder.getPlayerId().equals(player.getUniqueId())) { + event.setCancelled(true); + return; + } + event.setCancelled(true); + int raw = event.getRawSlot(); + if (raw >= event.getView().getTopInventory().getSize()) return; + + ClickType click = event.getClick(); + boolean left = click.isLeftClick(); + boolean right = click.isRightClick(); + boolean shift = click.isShiftClick(); + + BlocksGui.handleClick(plugin, player, raw, holder.getMineName(), + left, shift, right, + BlocksGui.getBlockAtSlot(event.getView().getTopInventory(), raw, plugin, holder.getMineName())); + } + + private void handleBlockMaterialSelectionGui(InventoryClickEvent event, BlockMaterialSelectionGuiHolder holder) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!holder.getPlayerId().equals(player.getUniqueId())) { + event.setCancelled(true); + return; + } + event.setCancelled(true); + int raw = event.getRawSlot(); + if (raw >= event.getView().getTopInventory().getSize()) return; + + BlockMaterialSelectionGui.handleClick(plugin, player, raw, holder.getMineName(), + holder.getPage(), event.getClick().isLeftClick()); + } + + private void handleResetGui(InventoryClickEvent event, ResetGuiHolder holder) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!holder.getPlayerId().equals(player.getUniqueId())) { + event.setCancelled(true); + return; + } + event.setCancelled(true); + int raw = event.getRawSlot(); + if (raw >= event.getView().getTopInventory().getSize()) return; + + ClickType click = event.getClick(); + ResetGui.handleClick(plugin, player, raw, holder.getMineName(), + click.isLeftClick(), click.isRightClick(), click.isShiftClick()); + } + + private void handleRewardsGui(InventoryClickEvent event, RewardsGuiHolder holder) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!holder.getPlayerId().equals(player.getUniqueId())) { + event.setCancelled(true); + return; + } + event.setCancelled(true); + int raw = event.getRawSlot(); + if (raw >= event.getView().getTopInventory().getSize()) return; + + ClickType click = event.getClick(); + int rewardIndex = raw < 45 ? raw : -1; + RewardsGui.handleClick(plugin, player, raw, holder.getMineName(), + click.isLeftClick(), click.isRightClick(), rewardIndex); + } + + private void handleConfirmDeleteGui(InventoryClickEvent event, ConfirmDeleteGuiHolder holder) { + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!holder.getPlayerId().equals(player.getUniqueId())) { + event.setCancelled(true); + return; + } + event.setCancelled(true); + int raw = event.getRawSlot(); + if (raw >= event.getView().getTopInventory().getSize()) return; + ConfirmDeleteGui.handleClick(plugin, player, raw, holder.getMineName()); + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + var holder = event.getInventory().getHolder(); + if (holder instanceof MineEditGuiHolder || + holder instanceof BlocksGuiHolder || + holder instanceof BlockMaterialSelectionGuiHolder || + holder instanceof ResetGuiHolder || + holder instanceof RewardsGuiHolder || + holder instanceof ConfirmDeleteGuiHolder) { + event.setCancelled(true); + } + } +} diff --git a/src/main/java/dev/loki/lomines/listener/player/PlayerInteractListener.java b/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java similarity index 64% rename from src/main/java/dev/loki/lomines/listener/player/PlayerInteractListener.java rename to src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java index 639f753..2c36951 100644 --- a/src/main/java/dev/loki/lomines/listener/player/PlayerInteractListener.java +++ b/src/main/java/dev/loki/lomines/listener/PlayerInteractListener.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.listener.player; +package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.gui.group.GroupCreateGui; @@ -48,44 +48,38 @@ public final class PlayerInteractListener implements Listener { if (action == Action.LEFT_CLICK_BLOCK && event.hasBlock()) { event.setCancelled(true); - handleLeftClick(player, event.getClickedBlock()); + Block block = event.getClickedBlock(); + if (block == null) { + return; + } + GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); + int slot = session.getActiveSlot(); + session.setPos1(slot, block.getLocation()); + player.sendMessage(Component.text( + "Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); + + // Start showing particles when selection is made + plugin.getWandParticleService().startShowingParticles(player); return; } if (action == Action.RIGHT_CLICK_BLOCK && !player.isSneaking()) { event.setCancelled(true); - handleRightClick(player, event.getClickedBlock()); + Block block = event.getClickedBlock(); + if (block == null) { + return; + } + GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); + int slot = session.getActiveSlot(); + session.setPos2(slot, block.getLocation()); + player.sendMessage(Component.text( + "Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); + + // Start showing particles when selection is made + plugin.getWandParticleService().startShowingParticles(player); } } - private void handleLeftClick(Player player, Block block) { - if (block == null) { - return; - } - GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); - int slot = session.getActiveSlot(); - session.setPos1(slot, block.getLocation()); - player.sendMessage(Component.text( - "Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); - - // Start showing particles when selection is made - plugin.getWandParticleService().startShowingParticles(player); - } - - private void handleRightClick(Player player, Block block) { - if (block == null) { - return; - } - GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId()); - int slot = session.getActiveSlot(); - session.setPos2(slot, block.getLocation()); - player.sendMessage(Component.text( - "Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN)); - - // Start showing particles when selection is made - plugin.getWandParticleService().startShowingParticles(player); - } - /** * Starts/stops particle visualization when player switches items. */ @@ -96,7 +90,9 @@ public final class PlayerInteractListener implements Listener { // Check if player switched to wand if (GroupWandItem.isWand(plugin, player.getInventory().getItem(event.getNewSlot()))) { plugin.getWandParticleService().startShowingParticles(player); - } else if (GroupWandItem.isWand(plugin, player.getInventory().getItem(event.getPreviousSlot()))) { // Check if player switched away from wand + } + // Check if player switched away from wand + else if (GroupWandItem.isWand(plugin, player.getInventory().getItem(event.getPreviousSlot()))) { plugin.getWandParticleService().stopShowingParticles(player.getUniqueId()); } } diff --git a/src/main/java/dev/loki/lomines/listener/player/PlayerJoinListener.java b/src/main/java/dev/loki/lomines/listener/PlayerJoinListener.java similarity index 94% rename from src/main/java/dev/loki/lomines/listener/player/PlayerJoinListener.java rename to src/main/java/dev/loki/lomines/listener/PlayerJoinListener.java index 9584891..699a1cd 100644 --- a/src/main/java/dev/loki/lomines/listener/player/PlayerJoinListener.java +++ b/src/main/java/dev/loki/lomines/listener/PlayerJoinListener.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.listener.player; +package dev.loki.lomines.listener; import dev.loki.lomines.LoMinesPlugin; import org.bukkit.event.EventHandler; diff --git a/src/main/java/dev/loki/lomines/listener/gui/GuiActionHandler.java b/src/main/java/dev/loki/lomines/listener/gui/GuiActionHandler.java deleted file mode 100644 index 80cc2c6..0000000 --- a/src/main/java/dev/loki/lomines/listener/gui/GuiActionHandler.java +++ /dev/null @@ -1,140 +0,0 @@ -package dev.loki.lomines.listener.gui; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.gui.confirm.ConfirmDeleteGui; -import dev.loki.lomines.gui.confirm.ConfirmDeleteGuiHolder; -import dev.loki.lomines.gui.mine.edit.blocks.select.BlockMaterialSelectionGui; -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.rewards.RewardsGui; -import dev.loki.lomines.gui.mine.holder.edit.BlockMaterialSelectionGuiHolder; -import dev.loki.lomines.gui.mine.holder.edit.BlocksGuiHolder; -import dev.loki.lomines.gui.mine.holder.edit.reset.ResetGuiHolder; -import dev.loki.lomines.gui.mine.holder.edit.rewards.RewardsGuiHolder; -import dev.loki.lomines.gui.mine.holder.main.MineEditGuiHolder; -import dev.loki.lomines.gui.mine.main.MineEditGui; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.event.inventory.InventoryClickEvent; - -public final class GuiActionHandler { - - private final LoMinesPlugin plugin; - - public GuiActionHandler(LoMinesPlugin plugin) { - this.plugin = plugin; - } - - public void handleMineEditGui(InventoryClickEvent event, MineEditGuiHolder holder) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (!holder.getPlayerId().equals(player.getUniqueId())) { - event.setCancelled(true); - return; - } - event.setCancelled(true); - int raw = event.getRawSlot(); - if (raw >= event.getView().getTopInventory().getSize()) { - return; - } - MineEditGui.handleClick(plugin, player, raw, holder.getMineName()); - } - - public void handleBlocksGui(InventoryClickEvent event, BlocksGuiHolder holder) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (!holder.getPlayerId().equals(player.getUniqueId())) { - event.setCancelled(true); - return; - } - event.setCancelled(true); - int raw = event.getRawSlot(); - if (raw >= event.getView().getTopInventory().getSize()) { - return; - } - - ClickType click = event.getClick(); - boolean left = click.isLeftClick(); - boolean right = click.isRightClick(); - boolean shift = click.isShiftClick(); - - BlocksGui.handleClick(plugin, player, raw, holder.getMineName(), - left, shift, right, - BlocksGui.getBlockAtSlot(event.getView().getTopInventory(), raw, plugin, holder.getMineName())); - } - - public void handleBlockMaterialSelectionGui(InventoryClickEvent event, BlockMaterialSelectionGuiHolder holder) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (!holder.getPlayerId().equals(player.getUniqueId())) { - event.setCancelled(true); - return; - } - event.setCancelled(true); - int raw = event.getRawSlot(); - if (raw >= event.getView().getTopInventory().getSize()) { - return; - } - - BlockMaterialSelectionGui.handleClick(plugin, player, raw, holder.getMineName(), - holder.getPage(), event.getClick().isLeftClick()); - } - - public void handleResetGui(InventoryClickEvent event, ResetGuiHolder holder) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (!holder.getPlayerId().equals(player.getUniqueId())) { - event.setCancelled(true); - return; - } - event.setCancelled(true); - int raw = event.getRawSlot(); - if (raw >= event.getView().getTopInventory().getSize()) { - return; - } - - ClickType click = event.getClick(); - ResetGui.handleClick(plugin, player, raw, holder.getMineName(), - click.isLeftClick(), click.isRightClick(), click.isShiftClick()); - } - - public void handleRewardsGui(InventoryClickEvent event, RewardsGuiHolder holder) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (!holder.getPlayerId().equals(player.getUniqueId())) { - event.setCancelled(true); - return; - } - event.setCancelled(true); - int raw = event.getRawSlot(); - if (raw >= event.getView().getTopInventory().getSize()) { - return; - } - - ClickType click = event.getClick(); - int rewardIndex = raw < 45 ? raw : -1; - RewardsGui.handleClick(plugin, player, raw, holder.getMineName(), - click.isLeftClick(), click.isRightClick(), rewardIndex); - } - - public void handleConfirmDeleteGui(InventoryClickEvent event, ConfirmDeleteGuiHolder holder) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (!holder.getPlayerId().equals(player.getUniqueId())) { - event.setCancelled(true); - return; - } - event.setCancelled(true); - int raw = event.getRawSlot(); - if (raw >= event.getView().getTopInventory().getSize()) { - return; - } - ConfirmDeleteGui.handleClick(plugin, player, raw, holder.getMineName()); - } -} diff --git a/src/main/java/dev/loki/lomines/listener/gui/MineEditGuiListener.java b/src/main/java/dev/loki/lomines/listener/gui/MineEditGuiListener.java deleted file mode 100644 index 4ba594d..0000000 --- a/src/main/java/dev/loki/lomines/listener/gui/MineEditGuiListener.java +++ /dev/null @@ -1,70 +0,0 @@ -package dev.loki.lomines.listener.gui; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.gui.confirm.ConfirmDeleteGuiHolder; -import dev.loki.lomines.gui.mine.holder.edit.BlockMaterialSelectionGuiHolder; -import dev.loki.lomines.gui.mine.holder.edit.BlocksGuiHolder; -import dev.loki.lomines.gui.mine.holder.edit.reset.ResetGuiHolder; -import dev.loki.lomines.gui.mine.holder.edit.rewards.RewardsGuiHolder; -import dev.loki.lomines.gui.mine.holder.main.MineEditGuiHolder; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.inventory.InventoryView; - -public final class MineEditGuiListener implements Listener { - - private final GuiActionHandler actionHandler; - - public MineEditGuiListener(LoMinesPlugin plugin) { - this.actionHandler = new GuiActionHandler(plugin); - } - - @EventHandler - public void onInventoryClick(InventoryClickEvent event) { - InventoryView view = event.getView(); - - if (view.getTopInventory().getHolder() instanceof MineEditGuiHolder holder) { - actionHandler.handleMineEditGui(event, holder); - return; - } - - if (view.getTopInventory().getHolder() instanceof BlocksGuiHolder holder) { - actionHandler.handleBlocksGui(event, holder); - return; - } - - if (view.getTopInventory().getHolder() instanceof BlockMaterialSelectionGuiHolder holder) { - actionHandler.handleBlockMaterialSelectionGui(event, holder); - return; - } - - if (view.getTopInventory().getHolder() instanceof ResetGuiHolder holder) { - actionHandler.handleResetGui(event, holder); - return; - } - - if (view.getTopInventory().getHolder() instanceof RewardsGuiHolder holder) { - actionHandler.handleRewardsGui(event, holder); - return; - } - - if (view.getTopInventory().getHolder() instanceof ConfirmDeleteGuiHolder holder) { - actionHandler.handleConfirmDeleteGui(event, holder); - } - } - - @EventHandler - public void onInventoryDrag(InventoryDragEvent event) { - var holder = event.getInventory().getHolder(); - if (holder instanceof MineEditGuiHolder || - holder instanceof BlocksGuiHolder || - holder instanceof BlockMaterialSelectionGuiHolder || - holder instanceof ResetGuiHolder || - holder instanceof RewardsGuiHolder || - holder instanceof ConfirmDeleteGuiHolder) { - event.setCancelled(true); - } - } -} diff --git a/src/main/java/dev/loki/lomines/util/ErrorHandler.java b/src/main/java/dev/loki/lomines/util/ErrorHandler.java index 2fcd715..39b1567 100644 --- a/src/main/java/dev/loki/lomines/util/ErrorHandler.java +++ b/src/main/java/dev/loki/lomines/util/ErrorHandler.java @@ -6,7 +6,7 @@ import java.util.logging.Level; import java.util.logging.Logger; /** - * Унифицированная обработка ошибок и отправка сообщений. + * Унифицированная обработка ошибок и отправка сообщений */ public class ErrorHandler { private final Logger logger; @@ -18,35 +18,35 @@ public class ErrorHandler { } /** - * Отправить сообщение об ошибке игроку. + * Отправить сообщение об ошибке игроку */ public void sendError(CommandSender sender, String message) { sender.sendMessage(MessageFormatter.error(message)); } /** - * Отправить сообщение об ошибке игроку с причиной. + * Отправить сообщение об ошибке игроку с причиной */ public void sendError(CommandSender sender, String message, String reason) { sender.sendMessage(MessageFormatter.error(message + ": " + reason)); } /** - * Залогировать ошибку. + * Залогировать ошибку */ public void logError(String message) { logger.log(Level.SEVERE, prefix + message); } /** - * Залогировать ошибку с исключением. + * Залогировать ошибку с исключением */ public void logError(String message, Throwable throwable) { logger.log(Level.SEVERE, prefix + message, throwable); } /** - * Отправить ошибку игроку и залогировать. + * Отправить ошибку игроку и залогировать */ public void handleError(CommandSender sender, String userMessage, String logMessage) { sendError(sender, userMessage); @@ -54,7 +54,7 @@ public class ErrorHandler { } /** - * Отправить ошибку игроку и залогировать с исключением. + * Отправить ошибку игроку и залогировать с исключением */ public void handleError(CommandSender sender, String userMessage, String logMessage, Throwable throwable) { sendError(sender, userMessage); @@ -62,21 +62,21 @@ public class ErrorHandler { } /** - * Обработать ошибку валидации. + * Обработать ошибку валидации */ public void handleValidationError(CommandSender sender, String fieldName, String value) { sendError(sender, "Некорректное значение для " + fieldName + ": " + value); } /** - * Обработать ошибку "не найдено". + * Обработать ошибку "не найдено" */ public void handleNotFound(CommandSender sender, String entityType, String identifier) { sendError(sender, entityType + " '" + identifier + "' не найден"); } /** - * Обработать ошибку доступа. + * Обработать ошибку доступа */ public void handlePermissionDenied(CommandSender sender, String action) { sendError(sender, "У вас нет прав для: " + action); diff --git a/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java b/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java index c2ec926..2c4125a 100644 --- a/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java +++ b/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java @@ -8,15 +8,25 @@ import org.bukkit.entity.Player; import java.util.Collection; +/** + * Utility for sending block updates to clients to prevent ghost blocks. + * + *

Ghost blocks occur when the server sets blocks without notifying clients, + * causing a desync where blocks appear invisible or behave strangely on the client. + * This utility forces block updates to be sent to all relevant players.

+ */ public final class BlockUpdateUtil { - private BlockUpdateUtil() { - } + private BlockUpdateUtil() {} + /** + * Sends a block update to all players who can see the specified chunk. + * This prevents ghost blocks by forcing the server to send the block state to clients. + * + * @param block the block to update + */ public static void sendBlockUpdate(Block block) { - if (block == null) { - return; - } + if (block == null) return; Chunk chunk = block.getChunk(); World world = block.getWorld(); @@ -28,6 +38,18 @@ public final class BlockUpdateUtil { } } + /** + * Sends block updates for all blocks in a cuboid region. + * Uses batching to minimize packet overhead. + * + * @param world the world containing the blocks + * @param minX minimum X coordinate + * @param minY minimum Y coordinate + * @param minZ minimum Z coordinate + * @param maxX maximum X coordinate + * @param maxY maximum Y coordinate + * @param maxZ maximum Z coordinate + */ public static void sendRegionUpdate(World world, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { Collection players = world.getPlayers(); @@ -46,15 +68,17 @@ public final class BlockUpdateUtil { } } + /** + * Sends updates for a list of locations. More efficient than region update + * when only specific positions need updating. + * + * @param locations the locations to update + */ public static void sendLocationsUpdate(java.util.List locations) { - if (locations == null || locations.isEmpty()) { - return; - } + if (locations == null || locations.isEmpty()) return; for (Location loc : locations) { - if (loc.getWorld() == null) { - continue; - } + if (loc.getWorld() == null) continue; Block block = loc.getBlock(); for (Player player : loc.getWorld().getPlayers()) { if (isLocationVisibleToPlayer(player, loc)) { @@ -64,6 +88,16 @@ public final class BlockUpdateUtil { } } + /** + * Refreshes chunks for all players in a region. + * This is a heavier operation but ensures complete chunk resync. + * + * @param world the world + * @param minX minimum block X + * @param minZ minimum block Z + * @param maxX maximum block X + * @param maxZ maximum block Z + */ public static void refreshChunks(World world, int minX, int minZ, int maxX, int maxZ) { int minChunkX = minX >> 4; int minChunkZ = minZ >> 4; @@ -75,7 +109,7 @@ public final class BlockUpdateUtil { Chunk chunk = world.getChunkAt(chunkX, chunkZ); if (chunk.isLoaded()) { for (Player player : world.getPlayers()) { - player.getWorld().refreshChunk(chunk.getX(), chunk.getZ()); + player.sendChunkUpdate(chunk); } } } @@ -83,9 +117,7 @@ public final class BlockUpdateUtil { } private static boolean isChunkVisibleToPlayer(Player player, Chunk chunk) { - if (!player.getWorld().equals(chunk.getWorld())) { - return false; - } + if (!player.getWorld().equals(chunk.getWorld())) return false; int renderDistance = player.getClientViewDistance(); int playerChunkX = player.getLocation().getBlockX() >> 4; @@ -98,9 +130,7 @@ public final class BlockUpdateUtil { } private static boolean isLocationVisibleToPlayer(Player player, Location loc) { - if (!player.getWorld().equals(loc.getWorld())) { - return false; - } + if (!player.getWorld().equals(loc.getWorld())) return false; double renderDistance = player.getClientViewDistance() * 16; return player.getLocation().distanceSquared(loc) <= renderDistance * renderDistance; diff --git a/src/main/java/dev/loki/lomines/util/block/SafeTeleportFinder.java b/src/main/java/dev/loki/lomines/util/block/SafeTeleportFinder.java deleted file mode 100644 index a59168c..0000000 --- a/src/main/java/dev/loki/lomines/util/block/SafeTeleportFinder.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.loki.lomines.util.block; - -import org.bukkit.Location; -import org.bukkit.block.Block; - -public final class SafeTeleportFinder { - - private SafeTeleportFinder() { - } - - public static Location findSafeTeleportLocation(Location target, int maxBlocksUp) { - if (target == null || target.getWorld() == null) { - return target; - } - - Location base = target.clone(); - for (int i = 0; i <= Math.max(0, maxBlocksUp); i++) { - Location candidate = base.clone().add(0, i, 0); - Block feet = candidate.getBlock(); - Block head = candidate.clone().add(0, 1, 0).getBlock(); - if (!feet.getType().isSolid() && !head.getType().isSolid()) { - return candidate; - } - } - return base; - } -} diff --git a/src/main/java/dev/loki/lomines/util/format/ChunkRefresher.java b/src/main/java/dev/loki/lomines/util/format/ChunkRefresher.java deleted file mode 100644 index 585b35e..0000000 --- a/src/main/java/dev/loki/lomines/util/format/ChunkRefresher.java +++ /dev/null @@ -1,84 +0,0 @@ -package dev.loki.lomines.util.format; - -import dev.loki.lomines.util.location.geo.Cuboid; -import org.bukkit.Chunk; -import org.bukkit.World; - -import java.lang.reflect.Method; -import java.util.HashSet; -import java.util.Set; - -public final class ChunkRefresher { - - private ChunkRefresher() { - } - - public static void refreshChunks(Cuboid region, boolean isPaper, Method sendChunkChangeMethod) { - Set chunks = getAffectedChunks(region); - World world = region.getWorld(); - - for (ChunkCoordinate coord : chunks) { - Chunk chunk = world.getChunkAt(coord.x, coord.z); - updateChunk(chunk, isPaper, sendChunkChangeMethod); - } - } - - private static void updateChunk(Chunk chunk, boolean isPaper, Method sendChunkChangeMethod) { - if (isPaper && sendChunkChangeMethod != null) { - updateChunkPaper(chunk, sendChunkChangeMethod); - } else { - updateChunkBukkit(chunk); - } - } - - private static void updateChunkPaper(Chunk chunk, Method sendChunkChangeMethod) { - try { - sendChunkChangeMethod.invoke(chunk); - } catch (Exception e) { - updateChunkBukkit(chunk); - } - } - - private static void updateChunkBukkit(Chunk chunk) { - World world = chunk.getWorld(); - try { - Method refreshChunk = World.class.getMethod("refreshChunk", int.class, int.class); - refreshChunk.invoke(world, chunk.getX(), chunk.getZ()); - } catch (Exception e) { - // chunks will update naturally - } - } - - private static Set getAffectedChunks(Cuboid region) { - Set chunks = new HashSet<>(); - int minChunkX = region.getMinX() >> 4; - int maxChunkX = region.getMaxX() >> 4; - int minChunkZ = region.getMinZ() >> 4; - int maxChunkZ = region.getMaxZ() >> 4; - - for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { - chunks.add(new ChunkCoordinate(chunkX, chunkZ)); - } - } - return chunks; - } - - private record ChunkCoordinate(int x, int z) { - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof ChunkCoordinate(int x1, int z1))) { - return false; - } - return x == x1 && z == z1; - } - - @Override - public int hashCode() { - return java.util.Objects.hash(x, z); - } - } -} diff --git a/src/main/java/dev/loki/lomines/util/format/ChunkUtils.java b/src/main/java/dev/loki/lomines/util/format/ChunkUtils.java index 78be928..7e00d8a 100644 --- a/src/main/java/dev/loki/lomines/util/format/ChunkUtils.java +++ b/src/main/java/dev/loki/lomines/util/format/ChunkUtils.java @@ -1,10 +1,20 @@ package dev.loki.lomines.util.format; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.Cuboid; import org.bukkit.Chunk; +import org.bukkit.World; import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; +/** + * Utility class for updating chunks after block modifications. + * Automatically detects Paper API availability and uses the most efficient method. + * + *

Paper detection is done through method reflection rather than Class.forName + * to provide a more reliable mechanism as per requirement 21.4.

+ */ public final class ChunkUtils { private static final boolean IS_PAPER = detectPaper(); @@ -14,15 +24,37 @@ public final class ChunkUtils { throw new UnsupportedOperationException("Utility class"); } + /** + * Updates all chunks within the given cuboid region. + * Uses Paper API if available for better performance, otherwise falls back to Bukkit API. + * + * @param region The cuboid region whose chunks should be updated + * @throws IllegalArgumentException if region is null + */ public static void updateChunks(Cuboid region) { if (region == null) { throw new IllegalArgumentException("Region cannot be null"); } - ChunkRefresher.refreshChunks(region, IS_PAPER, SEND_CHUNK_CHANGE_METHOD); + + Set chunks = getAffectedChunks(region); + World world = region.getWorld(); + + for (ChunkCoordinate coord : chunks) { + Chunk chunk = world.getChunkAt(coord.x, coord.z); + updateChunk(chunk); + } } + /** + * Detects if Paper API is available by checking for Paper-specific methods. + * This is more reliable than Class.forName as it checks for actual functionality. + * + * @return true if Paper API is available, false otherwise + */ private static boolean detectPaper() { try { + // Check for Paper-specific method in Chunk class + // Paper adds sendChunkChange methods that Bukkit doesn't have Class chunkClass = Chunk.class; chunkClass.getMethod("getPluginChunkTickets"); return true; @@ -31,15 +63,112 @@ public final class ChunkUtils { } } + /** + * Finds the sendChunkChange method if running on Paper. + * + * @return The sendChunkChange method, or null if not available + */ private static Method findSendChunkChangeMethod() { if (!IS_PAPER) { return null; } + try { Class chunkClass = Chunk.class; + // Paper 1.21 has sendChunkChange() method return chunkClass.getMethod("sendChunkChange"); } catch (NoSuchMethodException e) { return null; } } + + /** + * Updates a single chunk using the appropriate API. + * + * @param chunk The chunk to update + */ + private static void updateChunk(Chunk chunk) { + if (IS_PAPER && SEND_CHUNK_CHANGE_METHOD != null) { + updateChunkPaper(chunk); + } else { + updateChunkBukkit(chunk); + } + } + + /** + * Updates a chunk using Paper API. + * + * @param chunk The chunk to update + */ + private static void updateChunkPaper(Chunk chunk) { + try { + SEND_CHUNK_CHANGE_METHOD.invoke(chunk); + } catch (Exception e) { + // Fall back to Bukkit if Paper method fails + updateChunkBukkit(chunk); + } + } + + /** + * Updates a chunk using Bukkit API. + * Forces chunk to be resent to all players by marking it as modified. + * + * @param chunk The chunk to update + */ + private static void updateChunkBukkit(Chunk chunk) { + // Bukkit doesn't have a direct chunk update method + // The chunk will be automatically sent to players when they are nearby + // We can force a refresh by unloading and reloading, but that's expensive + // Instead, we rely on the natural chunk update mechanism + // Players will see the changes when the chunk is next sent to them + + // For Bukkit, we can use the World's refreshChunk method if available + World world = chunk.getWorld(); + try { + // Try to use refreshChunk if it exists (some Bukkit versions have it) + Method refreshChunk = World.class.getMethod("refreshChunk", int.class, int.class); + refreshChunk.invoke(world, chunk.getX(), chunk.getZ()); + } catch (Exception e) { + // If refreshChunk doesn't exist, chunks will update naturally + // This is acceptable as blocks are already set + } + } + + /** + * Calculates all chunk coordinates that are affected by the given region. + * + * @param region The cuboid region + * @return Set of chunk coordinates + */ + private static Set getAffectedChunks(Cuboid region) { + Set chunks = new HashSet<>(); + + // Convert block coordinates to chunk coordinates + int minChunkX = region.getMinX() >> 4; + int maxChunkX = region.getMaxX() >> 4; + int minChunkZ = region.getMinZ() >> 4; + int maxChunkZ = region.getMaxZ() >> 4; + + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + chunks.add(new ChunkCoordinate(chunkX, chunkZ)); + } + } + + return chunks; + } + + /** + * Simple immutable holder for chunk coordinates. + */ + private record ChunkCoordinate(int x, int z) { + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ChunkCoordinate other)) return false; + return x == other.x && z == other.z; + } + + } } diff --git a/src/main/java/dev/loki/lomines/util/format/color/ColorUtils.java b/src/main/java/dev/loki/lomines/util/format/color/ColorUtils.java deleted file mode 100644 index cd9af82..0000000 --- a/src/main/java/dev/loki/lomines/util/format/color/ColorUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.loki.lomines.util.format.color; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; - -public final class ColorUtils { - - private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); - private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder() - .hexColors() - .useUnusualXRepeatedCharacterHexFormat() - .build(); - - private ColorUtils() { - } - - public static Component format(String text) { - if (text == null || text.isEmpty()) { - return Component.empty(); - } - - if (text.contains("<") && text.contains(">")) { - try { - return MINI_MESSAGE.deserialize(text); - } catch (Exception ignored) { - } - } - - String miniMessageText = HexColorConverter.convertHexToMiniMessage(text); - miniMessageText = HexColorConverter.convertLegacyHexToMiniMessage(miniMessageText); - miniMessageText = LegacyColorConverter.convertLegacyToMiniMessage(miniMessageText); - - try { - return MINI_MESSAGE.deserialize(miniMessageText); - } catch (Exception e) { - return LEGACY_SERIALIZER.deserialize(text); - } - } - - public static String stripColors(String text) { - if (text == null || text.isEmpty()) { - return ""; - } - return text.replaceAll("&#[0-9A-Fa-f]{6}", "") - .replaceAll("&[0-9A-Fa-fk-orK-OR]", "") - .replaceAll("<[^>]+>", ""); - } - - public static String toLegacy(String text) { - if (text == null || text.isEmpty()) { - return ""; - } - - String result = HexColorConverter.convertMiniMessageToLegacyHex(text); - result = LegacyColorConverter.convertMiniMessageToLegacy(result); - return result; - } -} diff --git a/src/main/java/dev/loki/lomines/util/format/color/HexColorConverter.java b/src/main/java/dev/loki/lomines/util/format/color/HexColorConverter.java deleted file mode 100644 index 90cae2f..0000000 --- a/src/main/java/dev/loki/lomines/util/format/color/HexColorConverter.java +++ /dev/null @@ -1,61 +0,0 @@ -package dev.loki.lomines.util.format.color; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class HexColorConverter { - - private static final Pattern HEX_PATTERN = Pattern.compile("&#([0-9A-Fa-f]{6})"); - private static final Pattern LEGACY_HEX_PATTERN = - Pattern.compile("&x(&[0-9A-Fa-f])(&[0-9A-Fa-f])(&[0-9A-Fa-f])(&[0-9A-Fa-f])(&[0-9A-Fa-f])(&[0-9A-Fa-f])"); - private static final Pattern HEX_TAG_PATTERN = Pattern.compile("<#([0-9A-Fa-f]{6})>"); - - private HexColorConverter() { - } - - public static String convertHexToMiniMessage(String text) { - Matcher matcher = HEX_PATTERN.matcher(text); - StringBuffer result = new StringBuffer(); - while (matcher.find()) { - String hex = matcher.group(1); - matcher.appendReplacement(result, "<#" + hex + ">"); - } - matcher.appendTail(result); - return result.toString(); - } - - public static String convertLegacyHexToMiniMessage(String text) { - if (!text.contains("&x")) { - return text; - } - Matcher matcher = LEGACY_HEX_PATTERN.matcher(text); - StringBuffer result = new StringBuffer(); - while (matcher.find()) { - String r1 = matcher.group(1).substring(1); - String r2 = matcher.group(2).substring(1); - String g1 = matcher.group(3).substring(1); - String g2 = matcher.group(4).substring(1); - String b1 = matcher.group(5).substring(1); - String b2 = matcher.group(6).substring(1); - String hex = r1 + r2 + g1 + g2 + b1 + b2; - matcher.appendReplacement(result, "<#" + hex + ">"); - } - matcher.appendTail(result); - return result.toString(); - } - - public static String convertMiniMessageToLegacyHex(String text) { - Matcher matcher = HEX_TAG_PATTERN.matcher(text); - StringBuffer sb = new StringBuffer(); - while (matcher.find()) { - String hex = matcher.group(1); - StringBuilder legacy = new StringBuilder("&x"); - for (char c : hex.toCharArray()) { - legacy.append("&").append(c); - } - matcher.appendReplacement(sb, legacy.toString()); - } - matcher.appendTail(sb); - return sb.toString(); - } -} diff --git a/src/main/java/dev/loki/lomines/util/format/color/LegacyColorConverter.java b/src/main/java/dev/loki/lomines/util/format/color/LegacyColorConverter.java deleted file mode 100644 index fb3f64a..0000000 --- a/src/main/java/dev/loki/lomines/util/format/color/LegacyColorConverter.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.loki.lomines.util.format.color; - -public final class LegacyColorConverter { - - private LegacyColorConverter() { - } - - public static String convertLegacyToMiniMessage(String text) { - return text - .replace("&0", "") - .replace("&1", "") - .replace("&2", "") - .replace("&3", "") - .replace("&4", "") - .replace("&5", "") - .replace("&6", "") - .replace("&7", "") - .replace("&8", "") - .replace("&9", "") - .replace("&a", "") - .replace("&b", "") - .replace("&c", "") - .replace("&d", "") - .replace("&e", "") - .replace("&f", "") - .replace("&k", "") - .replace("&l", "") - .replace("&m", "") - .replace("&n", "") - .replace("&o", "") - .replace("&r", ""); - } - - public static String convertMiniMessageToLegacy(String text) { - return text - .replace("", "&0") - .replace("", "&1") - .replace("", "&2") - .replace("", "&3") - .replace("", "&4") - .replace("", "&5") - .replace("", "&6") - .replace("", "&7") - .replace("", "&8") - .replace("", "&9") - .replace("", "&a") - .replace("", "&b") - .replace("", "&c") - .replace("", "&d") - .replace("", "&e") - .replace("", "&f") - .replace("", "&k") - .replace("", "&l") - .replace("", "&m") - .replace("", "&n") - .replace("", "&o") - .replace("", "&r"); - } -} diff --git a/src/main/java/dev/loki/lomines/util/location/geo/Cuboid.java b/src/main/java/dev/loki/lomines/util/location/Cuboid.java similarity index 66% rename from src/main/java/dev/loki/lomines/util/location/geo/Cuboid.java rename to src/main/java/dev/loki/lomines/util/location/Cuboid.java index 334be62..cbd7597 100644 --- a/src/main/java/dev/loki/lomines/util/location/geo/Cuboid.java +++ b/src/main/java/dev/loki/lomines/util/location/Cuboid.java @@ -1,9 +1,14 @@ -package dev.loki.lomines.util.location.geo; +package dev.loki.lomines.util.location; import org.bukkit.Location; import org.bukkit.World; -/** Immutable 3D rectangular region in a Minecraft world. Thread-safe. */ +/** + * Immutable representation of a rectangular 3D region (cuboid) in a Minecraft world. + * Used to define mine boundaries and check block containment. + * + *

This class is thread-safe due to its immutability.

+ */ public final class Cuboid { private final World world; @@ -14,6 +19,14 @@ public final class Cuboid { private final int maxY; private final int maxZ; + /** + * Creates a cuboid from two corner locations. + * The locations must be in the same world. + * + * @param loc1 First corner location + * @param loc2 Second corner location (opposite corner) + * @throws IllegalArgumentException if locations are in different worlds or null + */ public Cuboid(Location loc1, Location loc2) { if (loc1 == null || loc2 == null) { throw new IllegalArgumentException("Locations cannot be null"); @@ -39,6 +52,12 @@ public final class Cuboid { this.maxZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ()); } + /** + * Checks if a location is contained within this cuboid. + * + * @param location The location to check + * @return true if the location is within this cuboid, false otherwise + */ public boolean contains(Location location) { if (location == null || location.getWorld() == null) { return false; @@ -57,6 +76,11 @@ public final class Cuboid { && z >= minZ && z <= maxZ; } + /** + * Calculates the volume of this cuboid in blocks. + * + * @return The number of blocks contained in this cuboid + */ public int getVolume() { int width = maxX - minX + 1; int height = maxY - minY + 1; @@ -64,30 +88,65 @@ public final class Cuboid { return width * height * depth; } + /** + * Gets the world this cuboid is in. + * + * @return The world + */ public World getWorld() { return world; } + /** + * Gets the minimum X coordinate. + * + * @return Minimum X + */ public int getMinX() { return minX; } + /** + * Gets the minimum Y coordinate. + * + * @return Minimum Y + */ public int getMinY() { return minY; } + /** + * Gets the minimum Z coordinate. + * + * @return Minimum Z + */ public int getMinZ() { return minZ; } + /** + * Gets the maximum X coordinate. + * + * @return Maximum X + */ public int getMaxX() { return maxX; } + /** + * Gets the maximum Y coordinate. + * + * @return Maximum Y + */ public int getMaxY() { return maxY; } + /** + * Gets the maximum Z coordinate. + * + * @return Maximum Z + */ public int getMaxZ() { return maxZ; } @@ -100,12 +159,8 @@ public final class Cuboid { @Override public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof Cuboid other)) { - return false; - } + if (this == obj) return true; + if (!(obj instanceof Cuboid other)) return false; return world.equals(other.world) && minX == other.minX && minY == other.minY && minZ == other.minZ diff --git a/src/main/java/dev/loki/lomines/util/location/LocationParser.java b/src/main/java/dev/loki/lomines/util/location/LocationParser.java index 9fb654e..dd6711b 100644 --- a/src/main/java/dev/loki/lomines/util/location/LocationParser.java +++ b/src/main/java/dev/loki/lomines/util/location/LocationParser.java @@ -42,10 +42,6 @@ public final class LocationParser { ); } - return buildLocation(matcher, locationString); - } - - private static Location buildLocation(Matcher matcher, String locationString) throws ConfigParseException { try { String worldName = matcher.group(1); double x = Double.parseDouble(matcher.group(2)); @@ -83,14 +79,10 @@ public final class LocationParser { return null; } String[] parts = locationString.split(";"); - if (parts.length < 4) { - return null; - } + if (parts.length < 4) return null; World world = Bukkit.getWorld(parts[0]); - if (world == null) { - return null; - } + if (world == null) return null; try { double x = Double.parseDouble(parts[1]); diff --git a/src/main/java/dev/loki/lomines/util/location/safe/SafeTeleportUtil.java b/src/main/java/dev/loki/lomines/util/location/SafeTeleportUtil.java similarity index 86% rename from src/main/java/dev/loki/lomines/util/location/safe/SafeTeleportUtil.java rename to src/main/java/dev/loki/lomines/util/location/SafeTeleportUtil.java index 2492096..232d843 100644 --- a/src/main/java/dev/loki/lomines/util/location/safe/SafeTeleportUtil.java +++ b/src/main/java/dev/loki/lomines/util/location/SafeTeleportUtil.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.util.location.safe; +package dev.loki.lomines.util.location; import org.bukkit.Location; import org.bukkit.World; @@ -10,8 +10,7 @@ import org.bukkit.block.Block; */ public final class SafeTeleportUtil { - private SafeTeleportUtil() { - } + private SafeTeleportUtil() {} /** * Finds a safe teleport location near the given destination. @@ -40,29 +39,17 @@ public final class SafeTeleportUtil { return destination; } - return findFallbackSafe(world, x, y, z, yaw, pitch, maxUpOffset); - } - - private static Location findFallbackSafe(World world, int x, int y, int z, float yaw, float pitch, int maxUpOffset) { Location result = findNearbySafe(world, x, y, z, yaw, pitch); - if (result != null) { - return result; - } + if (result != null) return result; result = searchUpward(world, x, y, z, yaw, pitch, maxUpOffset); - if (result != null) { - return result; - } + if (result != null) return result; result = searchDownward(world, x, y, z, yaw, pitch); - if (result != null) { - return result; - } + if (result != null) return result; result = searchDiagonal(world, x, y, z, yaw, pitch, maxUpOffset); - if (result != null) { - return result; - } + if (result != null) return result; return new Location(world, x + 0.5, y, z + 0.5, yaw, pitch); } @@ -105,8 +92,8 @@ public final class SafeTeleportUtil { private static Location searchDiagonal(World world, int x, int y, int z, float yaw, float pitch, int maxUpOffset) { int[][] nearbyDiagonal = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}}; for (int[] offset : nearbyDiagonal) { - for (int yo = -1; yo <= maxUpOffset; yo++) { - int newY = y + yo; + for (int yOffset = -1; yOffset <= maxUpOffset; yOffset++) { + int newY = y + yOffset; if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) { continue; } diff --git a/src/main/java/dev/loki/lomines/util/selection/MaskScanner.java b/src/main/java/dev/loki/lomines/util/selection/MaskScanner.java index 8be7b20..8746f62 100644 --- a/src/main/java/dev/loki/lomines/util/selection/MaskScanner.java +++ b/src/main/java/dev/loki/lomines/util/selection/MaskScanner.java @@ -1,6 +1,6 @@ package dev.loki.lomines.util.selection; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.Cuboid; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; diff --git a/src/main/java/dev/loki/lomines/util/selection/Selection.java b/src/main/java/dev/loki/lomines/util/selection/Selection.java index fbea6e6..328a0a0 100644 --- a/src/main/java/dev/loki/lomines/util/selection/Selection.java +++ b/src/main/java/dev/loki/lomines/util/selection/Selection.java @@ -1,6 +1,6 @@ package dev.loki.lomines.util.selection; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.Cuboid; import org.bukkit.Location; import java.util.ArrayList; diff --git a/src/main/java/dev/loki/lomines/wand/ParticleUtil.java b/src/main/java/dev/loki/lomines/wand/ParticleUtil.java index d49ed50..56c11b5 100644 --- a/src/main/java/dev/loki/lomines/wand/ParticleUtil.java +++ b/src/main/java/dev/loki/lomines/wand/ParticleUtil.java @@ -11,14 +11,11 @@ import org.bukkit.entity.Player; */ final class ParticleUtil { - private ParticleUtil() { - } + private ParticleUtil() {} static void showCornerParticles(Player player, Location loc, Color color) { World world = loc.getWorld(); - if (world == null) { - return; - } + if (world == null) return; Particle.DustOptions dustOptions = new Particle.DustOptions(color, 1.0f); @@ -35,15 +32,14 @@ final class ParticleUtil { static void showEdgeParticles(Player player, Location pos1, Location pos2, Color color) { World world = pos1.getWorld(); - if (world == null) { - return; - } + if (world == null) return; Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.8f); int minX = Math.min(pos1.getBlockX(), pos2.getBlockX()); int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX()); int minY = Math.min(pos1.getBlockY(), pos2.getBlockY()); + int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()); int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); @@ -55,8 +51,6 @@ final class ParticleUtil { spawnEdgeParticles(player, world, minX, minY, minZ, minX, minY, maxZ, step, dustOptions); spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, minY, maxZ, step, dustOptions); - int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); - // Top face edges spawnEdgeParticles(player, world, minX, maxY, minZ, maxX, maxY, minZ, step, dustOptions); spawnEdgeParticles(player, world, minX, maxY, maxZ, maxX, maxY, maxZ, step, dustOptions); @@ -70,15 +64,14 @@ final class ParticleUtil { spawnEdgeParticles(player, world, maxX, minY, maxZ, maxX, maxY, maxZ, step, dustOptions); } - @SuppressWarnings("checkstyle:ParameterNumber") private static void spawnEdgeParticles(Player player, World world, - double x1, double y1, double z1, - double x2, double y2, double z2, - double step, Particle.DustOptions dustOptions) { + double x1, double y1, double z1, + double x2, double y2, double z2, + double step, Particle.DustOptions dustOptions) { double distance = Math.sqrt( Math.pow(x2 - x1, 2) + - Math.pow(y2 - y1, 2) + - Math.pow(z2 - z1, 2) + Math.pow(y2 - y1, 2) + + Math.pow(z2 - z1, 2) ); int count = (int) (distance / step) + 1; @@ -98,9 +91,7 @@ final class ParticleUtil { static void showOutlineParticles(Player player, Location pos1, Location pos2, Color color, int density) { World world = pos1.getWorld(); - if (world == null || !world.equals(player.getWorld())) { - return; - } + if (world == null || !world.equals(player.getWorld())) return; Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.5f); @@ -112,14 +103,14 @@ final class ParticleUtil { int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); Location[] corners = { - new Location(world, minX + 0.5, minY + 0.5, minZ + 0.5), - new Location(world, maxX + 0.5, minY + 0.5, minZ + 0.5), - new Location(world, minX + 0.5, minY + 0.5, maxZ + 0.5), - new Location(world, maxX + 0.5, minY + 0.5, maxZ + 0.5), - new Location(world, minX + 0.5, maxY + 0.5, minZ + 0.5), - new Location(world, maxX + 0.5, maxY + 0.5, minZ + 0.5), - new Location(world, minX + 0.5, maxY + 0.5, maxZ + 0.5), - new Location(world, maxX + 0.5, maxY + 0.5, maxZ + 0.5) + new Location(world, minX + 0.5, minY + 0.5, minZ + 0.5), + new Location(world, maxX + 0.5, minY + 0.5, minZ + 0.5), + new Location(world, minX + 0.5, minY + 0.5, maxZ + 0.5), + new Location(world, maxX + 0.5, minY + 0.5, maxZ + 0.5), + new Location(world, minX + 0.5, maxY + 0.5, minZ + 0.5), + new Location(world, maxX + 0.5, maxY + 0.5, minZ + 0.5), + new Location(world, minX + 0.5, maxY + 0.5, maxZ + 0.5), + new Location(world, maxX + 0.5, maxY + 0.5, maxZ + 0.5) }; for (Location corner : corners) { diff --git a/src/main/java/dev/loki/lomines/wand/WandParticleService.java b/src/main/java/dev/loki/lomines/wand/WandParticleService.java index b20ea0b..9ca1c32 100644 --- a/src/main/java/dev/loki/lomines/wand/WandParticleService.java +++ b/src/main/java/dev/loki/lomines/wand/WandParticleService.java @@ -1,8 +1,6 @@ package dev.loki.lomines.wand; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.wand.group.GroupWandItem; -import dev.loki.lomines.wand.group.GroupWandSession; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -18,12 +16,13 @@ import java.util.concurrent.ConcurrentHashMap; */ public final class WandParticleService { + private final LoMinesPlugin plugin; + private final Map activeTasks = new ConcurrentHashMap<>(); + private static final Color COLOR_POS1 = Color.LIME; private static final Color COLOR_POS2 = Color.AQUA; private static final Color COLOR_EDGE = Color.YELLOW; private static final Color COLOR_READY = Color.GREEN; - private final LoMinesPlugin plugin; - private final Map activeTasks = new ConcurrentHashMap<>(); public WandParticleService(LoMinesPlugin plugin) { this.plugin = plugin; @@ -51,9 +50,7 @@ public final class WandParticleService { public void stopShowingParticles(UUID playerId) { BukkitRunnable task = activeTasks.remove(playerId); - if (task != null) { - task.cancel(); - } + if (task != null) task.cancel(); } public void stopAll() { @@ -94,9 +91,7 @@ public final class WandParticleService { private void showOtherSlots(Player player, GroupWandSession session, int activeSlot) { for (int i = 0; i < 9; i++) { - if (i == activeSlot) { - continue; - } + if (i == activeSlot) continue; if (session.isSlotReady(i)) { Location p1 = session.getPos1(i); Location p2 = session.getPos2(i); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 4f26a35..4878d37 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,42 +1,13 @@ -# ============================================ -# LoMines - Основная конфигурация -# ============================================ +# LoMines Configuration -# Включить статистику игроков +# Enable statistics tracking for players statistics-enabled: true -# Включить отладочный лог +# Enable debug logging debug: false -# ============================================ -# УВЕДОМЛЕНИЯ О СБРОСЕ ШАХТ -# ============================================ -# Здесь можно настроить глобальные уведомления -# Детальная настройка в messages.yml - -reset-notifications: - # Уведомлять о сбросе шахты - enabled: false - # Тип уведомления: CHAT, ACTIONBAR, TITLE, BOSSBAR, NONE - type: CHAT - # Всем игрокам на сервере (true) или только рядом с шахтой (false) - broadcast-global: false - # Дистанция от шахты если broadcast-global: false - range: 100 - # Предупреждать перед сбросом - warnings: - enabled: true - # За сколько секунд до сброса предупреждать - seconds-before: - - 60 - - 30 - - 10 - -# ============================================ -# WORLDGUARD ИНТЕГРАЦИЯ -# ============================================ +# WorldGuard Integration Settings worldguard: - # Автоматически создавать регионы для шахт - enabled: false - # Приоритет региона (выше = приоритетнее) + enabled: true + auto-create-regions: false region-priority: 10 diff --git a/src/main/resources/defaults.yml b/src/main/resources/defaults.yml index 0771696..9b60ad0 100644 --- a/src/main/resources/defaults.yml +++ b/src/main/resources/defaults.yml @@ -13,9 +13,9 @@ reset: percent-enabled: false # Commands to run on reset (%mine%, %player%, %blocks% placeholders) - commands: [ ] + commands: [] # - "broadcast Mine %mine% has been reset!" - # - "eco give %player% 10" + # - "eco give %player% 10" # Broadcast message on reset (empty = no broadcast) broadcast: "" @@ -28,7 +28,7 @@ blocks: # For MASK mode: which block marks fill positions mask: marker: "pink_concrete" - positions: [ ] + positions: [] # UI settings ui: @@ -43,7 +43,7 @@ ui: timer-format: "mm:ss" # Rewards for mining specific blocks -rewards: [ ] +rewards: [] # Example: # - chance: 5.0 # blocks: @@ -56,7 +56,7 @@ rewards: [ ] # - "Mined from the depths" # commands: # - "eco give %player% 100" -# prevent-drops: true + # prevent-drops: true # Teleport players on reset teleport: @@ -84,17 +84,20 @@ worldguard: region-template: "{mine_name}_{random_4}" # Region owners (player names or "uuid:") - owners: [ ] + owners: [] # - "player_name" - # - "uuid:12345678-1234-1234-1234-123456789012" + # - "uuid:12345678-1234-1234-1234-123456789012" # Region members (player names or "uuid:") - members: [ ] + members: [] # WorldGuard flags to set - # Можно добавить любые другие флаги: pvp, tnt, build, и т.д. flags: - - "block-break=allow" + - "passthrough=deny" + - "build=allow" + - "pvp=deny" + - "tnt=deny" + - "creeper-explosion=deny" # Automatically protect the region on creation protect-on-create: true diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml deleted file mode 100644 index 18f2344..0000000 --- a/src/main/resources/messages.yml +++ /dev/null @@ -1,123 +0,0 @@ -# ============================================ -# LoMines - Сообщения плагина -# ============================================ -# Поддержка цветов: &#RRGGBB (HEX), &a..&f, -# Плейсхолдеры: {player}, {mine}, {percent}, {time}, {blocks} - -# ============================================ -# УВЕДОМЛЕНИЯ О СБРОСЕ ШАХТ -# ============================================ - -# Когда шахта сброшена автоматически (по времени или проценту) -mine-reset-auto: - # Тип уведомления: CHAT, ACTIONBAR, TITLE, BOSSBAR, NONE - type: CHAT - # Дистанция от центра шахты (-1 = все игроки на сервере) - range: -1 - # Сообщение (для CHAT и ACTIONBAR) - message: "�FF00[Шахты] &#FFFF00{mine} �FF00сброшена!" - # Для TITLE (оставить пустым если не нужно) - title: "&#FFFF00{mine}" - subtitle: "�FF00Сброшена!" - # Для BOSSBAR (оставить пустым если не нужно) - bossbar: "&#FFFF00{mine} &#AAAAAA| Сброшена" - bossbar-color: GREEN - bossbar-style: SOLID - # Длительность в тиках (20 = 1 секунда, только для TITLE и BOSSBAR) - duration: 60 - -# Когда шахта сброшена вручную командой -mine-reset-manual: - type: CHAT - range: -1 - message: "�FF00[Шахты] &#FFFF00{mine} �FF00сброшена администратором!" - title: "" - subtitle: "" - bossbar: "" - bossbar-color: GREEN - bossbar-style: SOLID - duration: 60 - -# Предупреждение перед сбросом (за 60, 30, 10 секунд) -mine-reset-warning: - enabled: true - # За сколько секунд до сброса предупреждать - warnings: - - 60 - - 30 - - 10 - type: CHAT - range: 100 - message: "&#FFAA00[Шахты] &#FFFF00{mine} &#FFAA00сбросится через &#FF0000{time}" - title: "" - subtitle: "" - -# ============================================ -# СООБЩЕНИЯ КОМАНД -# ============================================ - -# Создание шахты -mine-created: "�FF00Шахта &#FFFF00{mine} �FF00успешно создана!" -mine-create-error: "&#FF0000Ошибка при создании шахты: {error}" - -# Удаление шахты -mine-deleted: "�FF00Шахта &#FFFF00{mine} �FF00удалена!" -mine-delete-error: "&#FF0000Ошибка при удалении шахты: {error}" - -# Перезагрузка шахты -mine-reloaded: "�FF00Шахта &#FFFF00{mine} �FF00перезагружена!" -mine-reload-error: "&#FF0000Ошибка при перезагрузке шахты: {error}" - -# Перезагрузка всех шахт -mines-reloaded: "�FF00Все шахты перезагружены! ({count} штук)" - -# Телепортация к шахте -mine-teleport: "�FF00Телепортировано к шахте &#FFFF00{mine}" -mine-teleport-no-location: "&#FF0000У шахты нет настроенной точки телепорта!" - -# Информация о шахте -mine-info-header: "󙀸========== &#FFD700{mine} 󙀸==========" -mine-info-blocks: "&#AAAAAAБлоков: &#FFFFFF{blocks}&#AAAAAA/&#FFFFFF{total} &#AAAAAA(&#FFFFFF{percent}%&#AAAAAA)" -mine-info-reset: "&#AAAAAAСледующий сброс через: &#FFFFFF{time}" -mine-info-footer: "󙀸==============================" - -# ============================================ -# СООБЩЕНИЯ ИГРОКАМ -# ============================================ - -# Когда игрок ломает блок в шахте -block-break-reward: "�FF00+&#FFD700{amount} &#AAAAAAза добычу блока!" - -# Когда игрок получает награду из random-rewards -reward-received: "�FF00Вы получили награду за добычу!" - -# ============================================ -# ОШИБКИ -# ============================================ - -no-permission: "&#FF0000У вас нет прав для этой команды!" -player-only: "&#FF0000Эта команда только для игроков!" -mine-not-found: "&#FF0000Шахта не найдена: &#FFFF00{mine}" -invalid-arguments: "&#FF0000Неверные аргументы команды!" -no-selection: "&#FF0000Выделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка" -wand-given: "�FF00Вы получили палочку для выделения шахты!" - -# ============================================ -# СТАТИСТИКА -# ============================================ - -stats-header: "󙀸========== &#FFD700Статистика {player} 󙀸==========" -stats-blocks: "&#AAAAAAБлоков добыто: &#FFFFFF{blocks}" -stats-resets: "&#AAAAAAШахт сброшено: &#FFFFFF{resets}" -stats-playtime: "&#AAAAAAВремя игры: &#FFFFFF{time}" -stats-rank: "&#AAAAAAПозиция в топе: &#FFFFFF#{rank}" -stats-footer: "󙀸======================================" - -# ============================================ -# ТОП ИГРОКОВ -# ============================================ - -top-header: "󙀸========== &#FFD700Топ по добыче 󙀸==========" -top-entry: "&#FFD700#{rank} &#AAAAAA| &#FFFFFF{player} &#AAAAAA| &#FFD700{blocks} блоков" -top-footer: "󙀸======================================" -top-empty: "&#AAAAAAТоп пуст - начните добывать блоки!" diff --git a/src/main/resources/mines/example_mine.yml b/src/main/resources/mines/example_mine.yml deleted file mode 100644 index fe1be65..0000000 --- a/src/main/resources/mines/example_mine.yml +++ /dev/null @@ -1,196 +0,0 @@ -# ============================================ -# LoMines - Пример конфигурации шахты -# ============================================ - -# Название шахты (для отображения в сообщениях и голограммах) -# Поддерживает цвета в формате &#RRGGBB (HEX) -# Пример: &#FF0000Красная �FF00шахта -name: "&#FFD700Золотая шахта&#FFFFFF" - -# ============================================ -# РЕГИОНЫ (области спавна блоков) -# ============================================ -# Можно указать несколько регионов (до 10 точек = 5 регионов) -# Каждый регион = пара точек (от угла до угла) -# Все регионы должны быть в ОДНОМ мире -# -# Формат: мир;x;y;z;yaw;pitch -# yaw/pitch можно опустить (будет 0;0) - -region: - selection: - '1': "world;-100.0;64.0;-100.0" - '2': "world;-90.0;74.0;-90.0" - # Можно добавить больше регионов: - # '3': "world;-80.0;64.0;-100.0" - # '4': "world;-70.0;74.0;-90.0" - -# ============================================ -# БЛОКИ И ШАНСЫ -# ============================================ -# Шансы суммируются и нормализуются автоматически -# Можно использовать материалы из Minecraft -# Для Oraxen: oraxen:id_предмета -# Для ItemsAdder: ia:id_предмета - -blocks: - contents: - stone: 50.0 - coal_ore: 20.0 - iron_ore: 15.0 - gold_ore: 10.0 - diamond_ore: 4.0 - emerald_ore: 1.0 - - # Режим заполнения: - # - cuboid: заполнить весь кубоид блоками - # - mask: только определённые позиции (см. ниже) - fill-mode: cuboid - - # Для режима mask (опционально): - # mask: - # marker: pink_concrete # Блок-маркер для позиций - # positions: [] # Позиции будут заполнены при сканировании - -# ============================================ -# СБРОС ШАХТЫ -# ============================================ -reset: - # Интервал сброса - # Форматы: 30s (секунды), 5m (минуты), 2h (часы), 1d (дни) - interval: "10m" - - # Процентный триггер (сброс при % заполненности) - # -1 или 0 = отключено - percent-trigger: -1.0 - percent-enabled: false - - # Сообщение при сбросе (пусто = стандартное) - # Поддерживает плейсхолдеры: {mine}, {time} - broadcast: "�FF00Шахта &#FFFF00{mine} �FF00сброшена!" - - # Команды при сбросе (опционально) - commands: [ ] - # - "eco give 100" - # - "broadcast Шахта {mine} сброшена!" - -# ============================================ -# ТЕЛЕПОРТАЦИЯ ПРИ СБРОСЕ -# ============================================ -# ИГРОКИ ТЕЛЕПОРТИРУЮТСЯ ТОЛЬКО ЕСЛИ ЗАСТРЯЛИ В БЛОКАХ! -# -# Точка телепорта - куда телепортировать при сбросе шахты -# (только если игрок оказался внутри блока после сброса) -teleport: - enabled: false - location: "" # Формат: мир;x;y;z;yaw;pitch - - # Точка спавна для "застрявших" игроков - # (если игрок оказался внутри блока после сброса) - # Если не задано - используется teleport.location - spawn: - enabled: false - location: "" # Формат: мир;x;y;z;yaw;pitch - -# ============================================ -# НАГРАДЫ ПРИ ДОБЫЧЕ -# ============================================ -# Выдаются с определённым шансом при ломании блоков - -rewards: [ ] - # Пример награды: - # - chance: 0.001 # 0.1% шанс - # prevent-drops: false # Отменить дроп блока? - # blocks: # На каких блоках работает - # - diamond_ore - # - emerald_ore - # items: # Предметы (опционально) - # - type: diamond - # amount: 1 - # name: "�FFFFРедкий алмаз" - # commands: # Команды (опционально) -# - "eco give 1000" - -# ============================================ -# ИНТЕРФЕЙС (UI) -# ============================================ -ui: - # ActionBar (полоска над хотбаром) - actionbar: - enabled: true - # Дистанция показа (в блоках) - range: 50.0 - # Формат строки - # Плейсхолдеры: {mine}, {percent}, {time}, {blocks}, {total} - format: "&#FFD700{mine} &#AAAAAA| �FF00{percent}% &#AAAAAA| &#FFFF00{time}" - - # Голограмма над шахтой (требует HolographicDisplays или DecentHolograms) - hologram: - enabled: false - # Высота голограммы от центра региона - height: 3.0 - format: - - "&#FFD700★ {mine} ★" - - "&#AAAAAAЗаполнено: �FF00{percent}%" - - "&#AAAAAAСброс через: &#FFFF00{time}" - - # Формат времени для плейсхолдеров - # 1 = HH:MM:SS (01:25:35) - # 2 = короткий (25m 35s) - # 3 = текстовый (1ч 25м 35с) - timer-format: 2 - -# ============================================ -# WORLDGUARD ИНТЕГРАЦИЯ (опционально) -# ============================================ -# Создаёт регион для защиты шахты -# Можно добавить ЛЮБЫЕ флаги WorldGuard! -worldguard: - enabled: false - # Шаблон названия региона - # {mine_name} = название шахты - # {random_4} = 4 случайных символа - region-template: "mine_{mine_name}_{random_4}" - - # Владельцы региона (uuid:xxx или имя) - owners: [ ] - - # Участники региона - members: [ ] - - # Флаги WorldGuard. По умолчанию: только block-break=allow - # МОЖНО ДОБАВИТЬ ЛЮБЫЕ ФЛАГИ: - # - "block-break=allow" # все могут ломать - # - "block-break=deny" # никто не может ломать - # - "block-break=-g members" # только владельцы/участники - # - "pvp=deny" # запретить PvP - # - "tnt=deny" # запретить TNT - # - "creeper-explosion=deny" # запретить взрывы криперов - # - "mob-spawning=deny" # запретить спавн мобов - # - "use=allow" # разрешить использование механизмов - flags: - - "block-break=allow" - -# ============================================ -# ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ -# ============================================ - -# Задержка между установкой блоков (в тиках, 20 тиков = 1 сек) -# 0 = мгновенно (может лагать на больших шахтах) -# Больше = плавнее, но медленнее -block-place-delay: 0 - -# Тип установки блоков: -# - fast: быстрый (рекомендуется) -# - bukkit: стандартный (медленнее, но стабильнее) -# - parallel: параллельный (экспериментально) -setter: fast - -# Разрешить физику блоков (песок, гравий падают)? -block-physics: false - -# Разрешить спавн сущностей в шахте? -entity-spawning: false - -# Разрешить течение жидкостей? -liquid-flow: false diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b8aca34..3495af0 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,6 +7,9 @@ api-version: "1.21" website: https://github.com/loki/lomines softdepend: [ PlaceholderAPI, WorldGuard, Oraxen, ItemsAdder ] +# LoAPI will handle dependency loading via DependencyManager +# Dependencies: commons-math3 and commons-io will be loaded at runtime + commands: lm: description: Main LoMines command diff --git a/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java b/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java index 2ccc173..c5c21fc 100644 --- a/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java +++ b/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java @@ -1,9 +1,8 @@ package dev.loki.lomines.command; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.command.admin.manage.AdminCommands; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.core.Mines; import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender; import org.junit.jupiter.api.BeforeEach; @@ -12,12 +11,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * Unit tests for AdminCommands class. @@ -49,7 +47,7 @@ class AdminCommandsTest { void testCreateMineSuccess() throws IOException { String mineName = "testmine"; - commands.handle(sender, "create", new String[]{mineName}); + commands.create(sender, mineName); verify(mines).create(mineName); verify(sender, times(2)).sendMessage(any(Component.class)); @@ -60,7 +58,7 @@ class AdminCommandsTest { String mineName = "existing"; doThrow(new IllegalArgumentException("Mine already exists")).when(mines).create(mineName); - commands.handle(sender, "create", new String[]{mineName}); + commands.create(sender, mineName); verify(sender).sendMessage(any(Component.class)); } @@ -69,7 +67,7 @@ class AdminCommandsTest { void testDeleteMineSuccess() throws IOException { String mineName = "testmine"; - commands.handle(sender, "delete", new String[]{mineName}); + commands.delete(sender, mineName); verify(mines).delete(mineName); verify(sender).sendMessage(any(Component.class)); @@ -80,8 +78,74 @@ class AdminCommandsTest { String mineName = "nonexistent"; doThrow(new IllegalArgumentException("Mine not found")).when(mines).delete(mineName); - commands.handle(sender, "delete", new String[]{mineName}); + commands.delete(sender, mineName); verify(sender).sendMessage(any(Component.class)); } + + @Test + void testResetMineSuccess() { + String mineName = "testmine"; + when(mines.get(mineName)).thenReturn(mine); + + commands.reset(sender, mineName, false); + + verify(mine).reset(false); + verify(sender).sendMessage(any(Component.class)); + } + + @Test + void testResetMineSilent() { + String mineName = "testmine"; + when(mines.get(mineName)).thenReturn(mine); + + commands.reset(sender, mineName, true); + + verify(mine).reset(true); + verify(sender).sendMessage(any(Component.class)); + } + + @Test + void testResetMineNotFound() { + String mineName = "nonexistent"; + when(mines.get(mineName)).thenThrow(new IllegalArgumentException("Mine not found")); + + commands.reset(sender, mineName, false); + + verify(sender).sendMessage(any(Component.class)); + } + + @Test + void testReloadSuccess() throws IOException { + Collection mineList = Arrays.asList(mine, mine); + when(mines.getAll()).thenReturn(mineList); + + commands.reload(sender); + + verify(mine, times(2)).stop(); + verify(mines).loadAll(); + verify(sender).sendMessage(any(Component.class)); + } + + @Test + void testListMinesEmpty() { + when(mines.getAll()).thenReturn(List.of()); + + commands.list(sender); + + verify(sender).sendMessage(any(Component.class)); + } + + @Test + void testListMinesWithData() { + when(mines.getAll()).thenReturn(Arrays.asList(mine, mine)); + when(mine.getName()).thenReturn("mine1"); + when(mine.getBlocks()).thenReturn(100); + when(mine.getTotalVolume()).thenReturn(200); + when(mine.getPercentFilled()).thenReturn(50.0); + + commands.list(sender); + + verify(sender, atLeast(3)).sendMessage(any(Component.class)); + } } diff --git a/src/test/java/dev/loki/lomines/command/AdminResetReloadTest.java b/src/test/java/dev/loki/lomines/command/AdminResetReloadTest.java deleted file mode 100644 index 9bee912..0000000 --- a/src/test/java/dev/loki/lomines/command/AdminResetReloadTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package dev.loki.lomines.command; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.command.admin.manage.AdminCommands; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.core.mine.registry.Mines; -import net.kyori.adventure.text.Component; -import org.bukkit.command.CommandSender; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class AdminResetReloadTest { - - @Mock - private LoMinesPlugin plugin; - - @Mock - private Mines mines; - - @Mock - private CommandSender sender; - - @Mock - private Mine mine; - - private AdminCommands commands; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - when(plugin.getMines()).thenReturn(mines); - commands = new AdminCommands(plugin); - } - - @Test - void testResetMineSuccess() { - String mineName = "testmine"; - when(mines.get(mineName)).thenReturn(mine); - - commands.handle(sender, "reset", new String[]{mineName}); - - verify(mine).reset(false); - verify(sender).sendMessage(any(Component.class)); - } - - @Test - void testResetMineSilent() { - String mineName = "testmine"; - when(mines.get(mineName)).thenReturn(mine); - - commands.handle(sender, "reset", new String[]{mineName, "silent"}); - - verify(mine).reset(true); - verify(sender).sendMessage(any(Component.class)); - } - - @Test - void testResetMineNotFound() { - String mineName = "nonexistent"; - when(mines.get(mineName)).thenThrow(new IllegalArgumentException("Mine not found")); - - commands.handle(sender, "reset", new String[]{mineName}); - - verify(sender).sendMessage(any(Component.class)); - } - - @Test - void testReloadSuccess() throws IOException { - Collection mineList = Arrays.asList(mine, mine); - when(mines.getAll()).thenReturn(mineList); - - commands.handle(sender, "reload", new String[0]); - - verify(mine, times(2)).stop(); - verify(mines).loadAll(); - verify(sender).sendMessage(any(Component.class)); - } - - @Test - void testListMinesEmpty() { - when(mines.getAll()).thenReturn(List.of()); - - commands.handle(sender, "list", new String[0]); - - verify(sender).sendMessage(any(Component.class)); - } - - @Test - void testListMinesWithData() { - when(mines.getAll()).thenReturn(Arrays.asList(mine, mine)); - when(mine.getName()).thenReturn("mine1"); - when(mine.getBlocks()).thenReturn(100); - when(mine.getTotalVolume()).thenReturn(200); - when(mine.getPercentFilled()).thenReturn(50.0); - - commands.handle(sender, "list", new String[0]); - - verify(sender, atLeast(3)).sendMessage(any(Component.class)); - } -} diff --git a/src/test/java/dev/loki/lomines/core/MineTest.java b/src/test/java/dev/loki/lomines/core/MineTest.java index e621892..84366f5 100644 --- a/src/test/java/dev/loki/lomines/core/MineTest.java +++ b/src/test/java/dev/loki/lomines/core/MineTest.java @@ -1,29 +1,23 @@ package dev.loki.lomines.core; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.data.config.block.BlockConfig; -import dev.loki.lomines.data.config.block.BlockKey; -import dev.loki.lomines.data.config.region.RegionConfig; -import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.block.BukkitBlockSetter; +import dev.loki.lomines.block.ItemsAdderBlockSetter; +import dev.loki.lomines.block.OraxenBlockSetter; +import dev.loki.lomines.data.MineConfig; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.World; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.HashMap; +import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; /** * Unit tests for Mine class, specifically testing BlockSetter creation logic. @@ -33,87 +27,91 @@ class MineTest { @Mock private LoMinesPlugin plugin; - private ServerMock server; + @Mock private World world; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - server = MockBukkit.mock(); - server.addSimpleWorld("world"); - world = server.getWorld("world"); - } - - @AfterEach - void tearDown() { - MockBukkit.unmock(); } @Test - void testCreateBlockSetterWithOraxenPrefixCreatesBukkitBlockSetter() { - // Oraxen integration is disabled — constructor throws IllegalArgumentException - Map blocks = new HashMap<>(); - blocks.put(new BlockKey.Oraxen("custom_ore"), 50.0); - blocks.put(new BlockKey.Oraxen("rare_ore"), 30.0); - blocks.put(new BlockKey.Oraxen("other_ore"), 20.0); - - MineConfig config = createTestConfig(blocks); - - assertThrows(IllegalArgumentException.class, - () -> new Mine("test_mine", config, plugin), - "Expected IllegalArgumentException when Oraxen integration is disabled"); - } - - @Test - void testCreateBlockSetterWithItemsAdderPrefixCreatesBukkitBlockSetter() { - // ItemsAdder integration is disabled — constructor throws IllegalArgumentException - Map blocks = new HashMap<>(); - blocks.put(new BlockKey.ItemsAdder("custom_block"), 60.0); - blocks.put(new BlockKey.ItemsAdder("special_block"), 40.0); - - MineConfig config = createTestConfig(blocks); - - assertThrows(IllegalArgumentException.class, - () -> new Mine("test_mine", config, plugin), - "Expected IllegalArgumentException when ItemsAdder integration is disabled"); - } - - @Test - void testCreateBlockSetterWithoutPrefixCreatesBukkitBlockSetter() { - Map blocks = new HashMap<>(); - blocks.put(new BlockKey.Vanilla(Material.STONE), 50.0); - blocks.put(new BlockKey.Vanilla(Material.COAL_ORE), 30.0); - blocks.put(new BlockKey.Vanilla(Material.IRON_ORE), 20.0); + void testCreateBlockSetter_WithOraxenPrefix_CreatesOraxenBlockSetter() { + // Arrange + Map blocks = new HashMap<>(); + blocks.put("oraxen:custom_ore", 50.0); + blocks.put("oraxen:rare_ore", 30.0); MineConfig config = createTestConfig(blocks); + // Act Mine mine = new Mine("test_mine", config, plugin); + // Assert assertNotNull(mine.getBlockSetter()); - assertInstanceOf(dev.loki.lomines.block.BukkitBlockSetter.class, mine.getBlockSetter(), - "Expected BukkitBlockSetter for vanilla blocks"); + assertInstanceOf(OraxenBlockSetter.class, mine.getBlockSetter(), "Expected OraxenBlockSetter for oraxen: prefix"); } @Test - void testCreateBlockSetterWithEmptyBlocksThrowsException() { - Map blocks = new HashMap<>(); + void testCreateBlockSetter_WithItemsAdderPrefix_CreatesItemsAdderBlockSetter() { + // Arrange + Map blocks = new HashMap<>(); + blocks.put("itemsadder:custom_block", 60.0); + blocks.put("itemsadder:special_block", 40.0); - assertThrows( - IllegalArgumentException.class, - () -> new BlockConfig(blocks, dev.loki.lomines.data.config.block.FillMode.CUBOID, null) - ); + MineConfig config = createTestConfig(blocks); + + // Act + Mine mine = new Mine("test_mine", config, plugin); + + // Assert + assertNotNull(mine.getBlockSetter()); + assertInstanceOf(ItemsAdderBlockSetter.class, mine.getBlockSetter(), "Expected ItemsAdderBlockSetter for itemsadder: prefix"); } - private MineConfig createTestConfig(Map blocks) { + @Test + void testCreateBlockSetter_WithoutPrefix_CreatesBukkitBlockSetter() { + // Arrange + Map blocks = new HashMap<>(); + blocks.put("STONE", 50.0); + blocks.put("COAL_ORE", 30.0); + blocks.put("IRON_ORE", 20.0); + + MineConfig config = createTestConfig(blocks); + + // Act + Mine mine = new Mine("test_mine", config, plugin); + + // Assert + assertNotNull(mine.getBlockSetter()); + assertInstanceOf(BukkitBlockSetter.class, mine.getBlockSetter(), "Expected BukkitBlockSetter for vanilla blocks without prefix"); + } + + @Test + void testCreateBlockSetter_WithEmptyBlocks_ThrowsException() { + // Arrange + Map blocks = new HashMap<>(); + MineConfig config = createTestConfig(blocks); + + // Act & Assert + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Mine("test_mine", config, plugin) + ); + + assertEquals("Mine must have at least one block type", exception.getMessage()); + } + + private MineConfig createTestConfig(Map blocks) { + when(world.getName()).thenReturn("world"); + Location loc1 = new Location(world, 0, 0, 0); Location loc2 = new Location(world, 10, 10, 10); - RegionConfig region = RegionConfig.single(new Cuboid(loc1, loc2)); - BlockConfig blockConfig = new BlockConfig(blocks, dev.loki.lomines.data.config.block.FillMode.CUBOID, null); - - return MineConfig.builder("test_mine") - .region(region) - .blocks(blockConfig) + return MineConfig.builder() + .selections(List.of(loc1, loc2)) + .blocks(blocks) + .resetTicks(6000) .build(); } } diff --git a/src/test/java/dev/loki/lomines/core/MineTickerTest.java b/src/test/java/dev/loki/lomines/core/MineTickerTest.java index 4789be2..566dfc6 100644 --- a/src/test/java/dev/loki/lomines/core/MineTickerTest.java +++ b/src/test/java/dev/loki/lomines/core/MineTickerTest.java @@ -1,8 +1,6 @@ package dev.loki.lomines.core; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.service.MineTicker; -import dev.loki.lomines.core.mine.registry.Mines; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -29,13 +27,13 @@ class MineTickerTest { } @Test - void testConstructionWithValidParametersDoesNotThrow() { + void testConstruction_WithValidParameters_DoesNotThrow() { // Act & Assert assertDoesNotThrow(() -> new MineTicker(mines, plugin)); } @Test - void testStopWhenNotRunningDoesNotThrow() { + void testStop_WhenNotRunning_DoesNotThrow() { // Arrange MineTicker ticker = new MineTicker(mines, plugin); @@ -44,7 +42,7 @@ class MineTickerTest { } @Test - void testMultipleStopsDoNotThrow() { + void testMultipleStops_DoNotThrow() { // Arrange MineTicker ticker = new MineTicker(mines, plugin); diff --git a/src/test/java/dev/loki/lomines/core/MinesLifecycleTest.java b/src/test/java/dev/loki/lomines/core/MinesLifecycleTest.java deleted file mode 100644 index b94c6cf..0000000 --- a/src/test/java/dev/loki/lomines/core/MinesLifecycleTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.loki.lomines.core; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.core.mine.registry.Mines; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.logging.Logger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class MinesLifecycleTest { - - @TempDir - Path tempDir; - - private Mines mines; - private LoMinesPlugin plugin; - private ServerMock server; - - @BeforeEach - void setUp() { - server = MockBukkit.mock(); - server.addSimpleWorld("world"); - - plugin = mock(LoMinesPlugin.class); - Logger logger = mock(Logger.class); - - when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); - when(plugin.getLogger()).thenReturn(logger); - - mines = new Mines(plugin); - } - - @AfterEach - void tearDown() { - MockBukkit.unmock(); - } - - @Test - void testCreateNewMine() throws IOException { - mines.create("newmine"); - - assertEquals(1, mines.getAll().size()); - assertTrue(mines.find("newmine").isPresent()); - Mine mine = mines.get("newmine"); - assertNotNull(mine); - assertEquals("newmine", mine.getName()); - assertTrue(Files.exists(tempDir.resolve("mines/newmine.yml"))); - verify(plugin.getLogger()).info("Created mine: newmine"); - } - - @Test - void testCreateMineCreatesFolder() throws IOException { - assertTrue(Files.isDirectory(tempDir.resolve("mines"))); - mines.create("testmine"); - assertTrue(Files.exists(tempDir.resolve("mines/testmine.yml"))); - } - - @Test - void testCreateDuplicateMineThrowsException() throws IOException { - mines.create("duplicate"); - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, - () -> mines.create("duplicate")); - assertTrue(ex.getMessage().contains("Mine already exists")); - } - - @Test - void testCreateMineIsCaseInsensitive() throws IOException { - mines.create("TestMine"); - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, - () -> mines.create("testmine")); - assertTrue(ex.getMessage().contains("Mine already exists")); - } -} diff --git a/src/test/java/dev/loki/lomines/core/MinesLoadingTest.java b/src/test/java/dev/loki/lomines/core/MinesLoadingTest.java deleted file mode 100644 index bf765e7..0000000 --- a/src/test/java/dev/loki/lomines/core/MinesLoadingTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package dev.loki.lomines.core; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.model.Mine; -import dev.loki.lomines.core.mine.registry.Mines; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.logging.Logger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class MinesLoadingTest { - - private static final String FULL_YAML = """ - selection.1: world;0;64;0;0.0;0.0 - selection.2: world;10;74;10;0.0;0.0 - contents: - stone: 100 - reset: - ticks: 6000 - percent: 10.0 - reset-on-percent: false - actionbar: - enabled: true - message: "Test mine" - range: 50.0 - timer-format: "mm:ss" - teleport-on-reset: false - reset-commands: [] - broadcast-reset: "" - random-rewards: [] - """; - - @TempDir - Path tempDir; - - private Mines mines; - private LoMinesPlugin plugin; - private ServerMock server; - - @BeforeEach - void setUp() { - server = MockBukkit.mock(); - server.addSimpleWorld("world"); - - plugin = mock(LoMinesPlugin.class); - Logger logger = mock(Logger.class); - - when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); - when(plugin.getLogger()).thenReturn(logger); - - mines = new Mines(plugin); - } - - @AfterEach - void tearDown() { - MockBukkit.unmock(); - } - - @Test - void testLoadAllWithValidMine() throws IOException { - Path minesFolder = tempDir.resolve("mines"); - Files.createDirectories(minesFolder); - Files.writeString(minesFolder.resolve("testmine.yml"), FULL_YAML); - - mines.loadAll(); - - assertEquals(1, mines.getAll().size()); - assertTrue(mines.find("testmine").isPresent()); - Mine mine = mines.get("testmine"); - assertNotNull(mine); - assertEquals("testmine", mine.getName()); - } - - @Test - void testLoadAllWithMultipleMines() throws IOException { - Path minesFolder = tempDir.resolve("mines"); - Files.createDirectories(minesFolder); - Files.writeString(minesFolder.resolve("mine1.yml"), FULL_YAML); - Files.writeString(minesFolder.resolve("mine2.yml"), FULL_YAML); - Files.writeString(minesFolder.resolve("mine3.yml"), FULL_YAML); - - mines.loadAll(); - - assertEquals(3, mines.getAll().size()); - assertTrue(mines.find("mine1").isPresent()); - assertTrue(mines.find("mine2").isPresent()); - assertTrue(mines.find("mine3").isPresent()); - } - - @Test - void testLoadAllIgnoresNonYamlFiles() throws IOException { - Path minesFolder = tempDir.resolve("mines"); - Files.createDirectories(minesFolder); - Files.writeString(minesFolder.resolve("validmine.yml"), FULL_YAML); - Files.writeString(minesFolder.resolve("readme.txt"), "not a mine config"); - Files.writeString(minesFolder.resolve("backup.bak"), "backup file"); - - mines.loadAll(); - - assertEquals(1, mines.getAll().size()); - assertTrue(mines.find("validmine").isPresent()); - } -} diff --git a/src/test/java/dev/loki/lomines/core/MinesTest.java b/src/test/java/dev/loki/lomines/core/MinesTest.java index bed91d6..91afc18 100644 --- a/src/test/java/dev/loki/lomines/core/MinesTest.java +++ b/src/test/java/dev/loki/lomines/core/MinesTest.java @@ -1,79 +1,400 @@ package dev.loki.lomines.core; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.registry.Mines; -import org.junit.jupiter.api.AfterEach; +import dev.lolib.core.LoLogger; +import dev.lolib.scheduler.ScheduledTask; +import dev.lolib.scheduler.Scheduler; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.block.data.BlockData; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; +import org.mockito.MockedStatic; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.logging.Logger; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; /** * Unit tests for Mines registry. + * Tests loading and creating mines. */ class MinesTest { @TempDir Path tempDir; - private Mines mines; private LoMinesPlugin plugin; - private ServerMock server; + private Scheduler scheduler; @BeforeEach void setUp() { - server = MockBukkit.mock(); - server.addSimpleWorld("world"); - plugin = mock(LoMinesPlugin.class); Logger logger = mock(Logger.class); + LoLogger loLogger = mock(LoLogger.class); + scheduler = mock(Scheduler.class); when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); when(plugin.getLogger()).thenReturn(logger); + when(plugin.loLogger()).thenReturn(loLogger); mines = new Mines(plugin); } - @AfterEach - void tearDown() { - MockBukkit.unmock(); + /** + * Helper method to set up Bukkit and Scheduler mocking for tests that create mines. + */ + private void setupBukkitAndScheduler(MockedStatic bukkit, MockedStatic schedulerStatic) { + Server server = mock(Server.class); + World world = mock(World.class); + BlockData blockData = mock(BlockData.class); + + bukkit.when(Bukkit::getServer).thenReturn(server); + bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); + bukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData); + + when(world.getName()).thenReturn("world"); + + schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler); + + // Mock scheduler methods to execute callbacks immediately for testing + ScheduledTask task = mock(ScheduledTask.class); + when(scheduler.runTimer(any(Runnable.class), anyLong(), anyLong())).thenReturn(task); } @Test void testLoadAllWithNoMinesFolder() throws IOException { + // When mines folder doesn't exist, loadAll should not throw assertDoesNotThrow(() -> mines.loadAll()); + + // No mines should be loaded assertEquals(0, mines.getAll().size()); } @Test void testLoadAllWithEmptyMinesFolder() throws IOException { - Files.createDirectories(tempDir.resolve("mines")); + // Create empty mines folder + Path minesFolder = tempDir.resolve("mines"); + Files.createDirectories(minesFolder); + + // Load all mines mines.loadAll(); + + // No mines should be loaded assertEquals(0, mines.getAll().size()); } @Test - void testLoadAllWithInvalidMine() throws IOException { + void testLoadAllWithValidMine() throws IOException { + // Create mines folder Path minesFolder = tempDir.resolve("mines"); Files.createDirectories(minesFolder); - Files.writeString(minesFolder.resolve("invalidmine.yml"), "selection.1: world;0;64;0;0.0;0.0\n"); - assertDoesNotThrow(() -> mines.loadAll()); - assertEquals(0, mines.getAll().size()); - verify(plugin.getLogger(), atLeastOnce()).severe(anyString()); + // Create a valid mine config file + String yamlContent = """ + selection.1: world;0;64;0;0.0;0.0 + selection.2: world;10;74;10;0.0;0.0 + contents: + stone: 100 + reset: + ticks: 6000 + percent: 10.0 + reset-on-percent: false + actionbar: + enabled: true + message: "Test mine" + range: 50.0 + timer-format: "mm:ss" + teleport-on-reset: false + reset-commands: [] + broadcast-reset: "" + random-rewards: [] + """; + + Path configFile = minesFolder.resolve("testmine.yml"); + Files.writeString(configFile, yamlContent); + + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Load all mines + mines.loadAll(); + + // Verify mine was loaded + assertEquals(1, mines.getAll().size()); + assertTrue(mines.find("testmine").isPresent()); + + Mine mine = mines.get("testmine"); + assertNotNull(mine); + assertEquals("testmine", mine.getName()); + } + } + + @Test + void testLoadAllWithMultipleMines() throws IOException { + // Create mines folder + Path minesFolder = tempDir.resolve("mines"); + Files.createDirectories(minesFolder); + + // Create multiple mine config files + String yamlContent = """ + selection.1: world;0;64;0;0.0;0.0 + selection.2: world;10;74;10;0.0;0.0 + contents: + stone: 100 + reset: + ticks: 6000 + percent: 10.0 + reset-on-percent: false + actionbar: + enabled: true + message: "Test mine" + range: 50.0 + timer-format: "mm:ss" + teleport-on-reset: false + reset-commands: [] + broadcast-reset: "" + random-rewards: [] + """; + + Files.writeString(minesFolder.resolve("mine1.yml"), yamlContent); + Files.writeString(minesFolder.resolve("mine2.yml"), yamlContent); + Files.writeString(minesFolder.resolve("mine3.yml"), yamlContent); + + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Load all mines + mines.loadAll(); + + // Verify all mines were loaded + assertEquals(3, mines.getAll().size()); + assertTrue(mines.find("mine1").isPresent()); + assertTrue(mines.find("mine2").isPresent()); + assertTrue(mines.find("mine3").isPresent()); + } + } + + @Test + void testLoadAllIgnoresNonYamlFiles() throws IOException { + // Create mines folder + Path minesFolder = tempDir.resolve("mines"); + Files.createDirectories(minesFolder); + + // Create a valid mine config file + String yamlContent = """ + selection.1: world;0;64;0;0.0;0.0 + selection.2: world;10;74;10;0.0;0.0 + contents: + stone: 100 + reset: + ticks: 6000 + percent: 10.0 + """; + + Files.writeString(minesFolder.resolve("validmine.yml"), yamlContent); + Files.writeString(minesFolder.resolve("readme.txt"), "This is not a mine config"); + Files.writeString(minesFolder.resolve("backup.bak"), "Backup file"); + + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Load all mines + mines.loadAll(); + + // Only the .yml file should be loaded + assertEquals(1, mines.getAll().size()); + assertTrue(mines.find("validmine").isPresent()); + } + } + + @Test + void testLoadAllWithInvalidMine() throws IOException { + // Create mines folder + Path minesFolder = tempDir.resolve("mines"); + Files.createDirectories(minesFolder); + + // Create an invalid mine config file (missing required fields) + String invalidYaml = """ + selection.1: world;0;64;0;0.0;0.0 + """; + + Files.writeString(minesFolder.resolve("invalidmine.yml"), invalidYaml); + + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Load all mines - should not throw, but should log error + assertDoesNotThrow(() -> mines.loadAll()); + + // Invalid mine should not be loaded + assertEquals(0, mines.getAll().size()); + + // Verify error was logged + verify(plugin.loLogger(), atLeastOnce()).error(anyString()); + } + } + + @Test + void testCreateNewMine() throws IOException { + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Create a new mine + mines.create("newmine"); + + // Verify mine was created + assertEquals(1, mines.getAll().size()); + assertTrue(mines.find("newmine").isPresent()); + + Mine mine = mines.get("newmine"); + assertNotNull(mine); + assertEquals("newmine", mine.getName()); + + // Verify config file was created + Path configFile = tempDir.resolve("mines/newmine.yml"); + assertTrue(Files.exists(configFile)); + + // Verify logger was called + verify(plugin.loLogger()).info("Created mine: newmine"); + } + } + + @Test + void testCreateMineCreatesFolder() throws IOException { + // Ensure mines folder doesn't exist + Path minesFolder = tempDir.resolve("mines"); + assertFalse(Files.exists(minesFolder)); + + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Create a new mine + mines.create("testmine"); + + // Verify mines folder was created + assertTrue(Files.exists(minesFolder)); + assertTrue(Files.isDirectory(minesFolder)); + } + } + + @Test + void testCreateDuplicateMineThrowsException() throws IOException { + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Create a mine + mines.create("duplicate"); + + // Try to create the same mine again + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + mines.create("duplicate"); + }); + + assertTrue(exception.getMessage().contains("Mine already exists")); + } + } + + @Test + void testCreateMineIsCaseInsensitive() throws IOException { + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Create a mine with mixed case + mines.create("TestMine"); + + // Try to create the same mine with different case + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + mines.create("testmine"); + }); + + assertTrue(exception.getMessage().contains("Mine already exists")); + } + } + + @Test + void testGetMineByName() throws IOException { + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Create a mine + mines.create("testmine"); + + // Get mine by exact name + Mine mine = mines.get("testmine"); + assertNotNull(mine); + assertEquals("testmine", mine.getName()); + + // Get mine by different case + Mine mine2 = mines.get("TestMine"); + assertNotNull(mine2); + assertEquals("testmine", mine2.getName()); + + // Same mine instance + assertSame(mine, mine2); + } + } + + @Test + void testGetNonExistentMineThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + mines.get("nonexistent"); + }); + + assertTrue(exception.getMessage().contains("Mine not found")); + } + + @Test + void testFindMineReturnsOptional() throws IOException { + // Mock Bukkit and Scheduler + try (MockedStatic bukkit = mockStatic(Bukkit.class); + MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + + setupBukkitAndScheduler(bukkit, schedulerStatic); + + // Create a mine + mines.create("testmine"); + + // Find existing mine + assertTrue(mines.find("testmine").isPresent()); + + // Find non-existent mine + assertFalse(mines.find("nonexistent").isPresent()); + } } } diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardCacheIntegrationTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardCacheIntegrationTest.java index 8196025..9bbba0e 100644 --- a/src/test/java/dev/loki/lomines/data/LeaderboardCacheIntegrationTest.java +++ b/src/test/java/dev/loki/lomines/data/LeaderboardCacheIntegrationTest.java @@ -1,9 +1,7 @@ package dev.loki.lomines.data; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.model.Leaderboard; -import dev.loki.lomines.data.stats.model.LeaderboardEntry; -import dev.loki.lomines.data.stats.service.StatsManager; +import dev.lolib.core.LoLogger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,9 +27,11 @@ class LeaderboardCacheIntegrationTest { void setUp() { // Create a mock plugin LoMinesPlugin mockPlugin = mock(LoMinesPlugin.class); + LoLogger loLogger = mock(LoLogger.class); File mockDataFolder = new File(System.getProperty("java.io.tmpdir"), "lomines-test"); when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder); when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger")); + when(mockPlugin.loLogger()).thenReturn(loLogger); statsManager = new StatsManager(mockPlugin); leaderboard = statsManager.getLeaderboard(); @@ -40,6 +40,7 @@ class LeaderboardCacheIntegrationTest { @Test void testCacheInvalidatedOnIncrementBlocks() { UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); // Set initial stats statsManager.getOrCreate(player1).setTotalBlocks(100); @@ -47,10 +48,9 @@ class LeaderboardCacheIntegrationTest { // Build cache List top1 = leaderboard.getTopTotal(10); assertEquals(1, top1.size(), "Should have 1 player initially"); - assertEquals(player1, top1.get(0).playerId()); + assertEquals(player1, top1.get(0).getPlayerId()); // Increment blocks for player2 - this should invalidate cache - UUID player2 = UUID.randomUUID(); statsManager.incrementBlocks(player2, "mine1"); // Get top again - should rebuild cache with new player @@ -58,8 +58,8 @@ class LeaderboardCacheIntegrationTest { assertEquals(2, top2.size(), "Should have 2 players after increment"); // Verify order (player1 has 100, player2 has 1) - assertEquals(player1, top2.get(0).playerId(), "Player1 should still be first"); - assertEquals(player2, top2.get(1).playerId(), "Player2 should be second"); + assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should still be first"); + assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second"); } @Test @@ -73,7 +73,7 @@ class LeaderboardCacheIntegrationTest { // Build cache List top1 = leaderboard.getTopTotal(10); - assertEquals(player2, top1.get(0).playerId(), "Player2 should be first initially"); + assertEquals(player2, top1.get(0).getPlayerId(), "Player2 should be first initially"); // Increment player1 many times to overtake player2 for (int i = 0; i < 60; i++) { @@ -82,10 +82,10 @@ class LeaderboardCacheIntegrationTest { // Get top again - cache should be rebuilt with new order List top2 = leaderboard.getTopTotal(10); - assertEquals(player1, top2.get(0).playerId(), "Player1 should be first after increments"); - assertEquals(110, top2.get(0).count(), "Player1 should have 110 blocks"); - assertEquals(player2, top2.get(1).playerId(), "Player2 should be second"); - assertEquals(100, top2.get(1).count(), "Player2 should still have 100 blocks"); + assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should be first after increments"); + assertEquals(110, top2.get(0).getCount(), "Player1 should have 110 blocks"); + assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second"); + assertEquals(100, top2.get(1).getCount(), "Player2 should still have 100 blocks"); } @Test @@ -104,6 +104,53 @@ class LeaderboardCacheIntegrationTest { // Get top - should show updated count List top2 = leaderboard.getTopTotal(10); assertEquals(1, top2.size()); - assertEquals(5, top2.get(0).count(), "Should have 5 blocks after 5 increments"); + assertEquals(5, top2.get(0).getCount(), "Should have 5 blocks after 5 increments"); + } + + @Test + void testCachePerformance() { + // Add many players + for (int i = 0; i < 100; i++) { + UUID playerId = UUID.randomUUID(); + statsManager.getOrCreate(playerId).setTotalBlocks(i + 1); + } + + // First call - builds cache (slower) + long start1 = System.nanoTime(); + List top1 = leaderboard.getTopTotal(10); + long time1 = System.nanoTime() - start1; + + // Second call - uses cache (should be much faster) + long start2 = System.nanoTime(); + List top2 = leaderboard.getTopTotal(10); + long time2 = System.nanoTime() - start2; + + assertEquals(10, top1.size()); + assertEquals(10, top2.size()); + + // Cache hit should be significantly faster (at least 2x) + assertTrue(time2 < time1, + String.format("Cache hit (%d ns) should be faster than cache miss (%d ns)", time2, time1)); + } + + @Test + void testDifferentLimitsUseSameCache() { + UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); + UUID player3 = UUID.randomUUID(); + + statsManager.getOrCreate(player1).setTotalBlocks(100); + statsManager.getOrCreate(player2).setTotalBlocks(200); + statsManager.getOrCreate(player3).setTotalBlocks(300); + + // Build cache with limit 10 + List top10 = leaderboard.getTopTotal(10); + assertEquals(3, top10.size()); + + // Get with limit 2 - should use same cache + List top2 = leaderboard.getTopTotal(2); + assertEquals(2, top2.size()); + assertEquals(player3, top2.get(0).getPlayerId(), "Should have highest player"); + assertEquals(player2, top2.get(1).getPlayerId(), "Should have second highest player"); } } diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardIntegrationTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardIntegrationTest.java deleted file mode 100644 index 36095d5..0000000 --- a/src/test/java/dev/loki/lomines/data/LeaderboardIntegrationTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package dev.loki.lomines.data; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.model.Leaderboard; -import dev.loki.lomines.data.stats.model.LeaderboardEntry; -import dev.loki.lomines.data.stats.service.StatsManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class LeaderboardIntegrationTest { - - private StatsManager statsManager; - private Leaderboard leaderboard; - - @BeforeEach - void setUp() { - LoMinesPlugin mockPlugin = mock(LoMinesPlugin.class); - File mockDataFolder = new File(System.getProperty("java.io.tmpdir"), "lomines-test"); - when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder); - when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger")); - - statsManager = new StatsManager(mockPlugin); - leaderboard = statsManager.getLeaderboard(); - } - - @Test - void testCachePerformance() { - for (int i = 0; i < 100; i++) { - UUID playerId = UUID.randomUUID(); - statsManager.getOrCreate(playerId).setTotalBlocks(i + 1); - } - - long start1 = System.nanoTime(); - List top1 = leaderboard.getTopTotal(10); - long time1 = System.nanoTime() - start1; - - long start2 = System.nanoTime(); - List top2 = leaderboard.getTopTotal(10); - long time2 = System.nanoTime() - start2; - - assertEquals(10, top1.size()); - assertEquals(10, top2.size()); - - assertTrue(time2 < time1, - String.format("Cache hit (%d ns) should be faster than cache miss (%d ns)", time2, time1)); - } - - @Test - void testDifferentLimitsUseSameCache() { - UUID player1 = UUID.randomUUID(); - UUID player2 = UUID.randomUUID(); - UUID player3 = UUID.randomUUID(); - - statsManager.getOrCreate(player1).setTotalBlocks(100); - statsManager.getOrCreate(player2).setTotalBlocks(200); - statsManager.getOrCreate(player3).setTotalBlocks(300); - - List top10 = leaderboard.getTopTotal(10); - assertEquals(3, top10.size()); - - List top2 = leaderboard.getTopTotal(2); - assertEquals(2, top2.size()); - assertEquals(player3, top2.get(0).playerId(), "Should have highest player"); - assertEquals(player2, top2.get(1).playerId(), "Should have second highest player"); - } -} diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardPositionTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardPositionTest.java deleted file mode 100644 index 87d51e8..0000000 --- a/src/test/java/dev/loki/lomines/data/LeaderboardPositionTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.loki.lomines.data; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.model.Leaderboard; -import dev.loki.lomines.data.stats.service.StatsManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class LeaderboardPositionTest { - - private StatsManager statsManager; - private Leaderboard leaderboard; - - @BeforeEach - void setUp() { - LoMinesPlugin mockPlugin = mock(LoMinesPlugin.class); - File mockDataFolder = new File(System.getProperty("java.io.tmpdir"), "lomines-test"); - when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder); - when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger")); - - statsManager = new StatsManager(mockPlugin); - leaderboard = statsManager.getLeaderboard(); - } - - @Test - void testGetPositionReturnsCorrectPosition() { - UUID player1 = UUID.randomUUID(); - UUID player2 = UUID.randomUUID(); - UUID player3 = UUID.randomUUID(); - - statsManager.getOrCreate(player1).setTotalBlocks(50); - statsManager.getOrCreate(player2).setTotalBlocks(200); - statsManager.getOrCreate(player3).setTotalBlocks(100); - - assertEquals(1, leaderboard.getPosition(player2), "Player with most blocks should be position 1"); - assertEquals(2, leaderboard.getPosition(player3), "Player with second most blocks should be position 2"); - assertEquals(3, leaderboard.getPosition(player1), "Player with least blocks should be position 3"); - } - - @Test - void testGetPositionReturnsMinusOneForNonExistentPlayer() { - UUID playerId = UUID.randomUUID(); - assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for non-existent player"); - } - - @Test - void testGetPositionReturnsMinusOneForZeroBlocks() { - UUID playerId = UUID.randomUUID(); - statsManager.getOrCreate(playerId).setTotalBlocks(0); - - assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for player with 0 blocks"); - } -} diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardQueryTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardQueryTest.java deleted file mode 100644 index 0368be4..0000000 --- a/src/test/java/dev/loki/lomines/data/LeaderboardQueryTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.loki.lomines.data; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.model.Leaderboard; -import dev.loki.lomines.data.stats.model.LeaderboardEntry; -import dev.loki.lomines.data.stats.service.StatsManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class LeaderboardQueryTest { - - private StatsManager statsManager; - private Leaderboard leaderboard; - - @BeforeEach - void setUp() { - LoMinesPlugin mockPlugin = mock(LoMinesPlugin.class); - File mockDataFolder = new File(System.getProperty("java.io.tmpdir"), "lomines-test"); - when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder); - when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger")); - - statsManager = new StatsManager(mockPlugin); - leaderboard = statsManager.getLeaderboard(); - } - - @Test - void testGetTopByMineSortedDescending() { - UUID player1 = UUID.randomUUID(); - UUID player2 = UUID.randomUUID(); - UUID player3 = UUID.randomUUID(); - - statsManager.getOrCreate(player1).setMineBlocks("mine1", 50); - statsManager.getOrCreate(player2).setMineBlocks("mine1", 200); - statsManager.getOrCreate(player3).setMineBlocks("mine1", 100); - - List top = leaderboard.getTopByMine("mine1", 10); - assertEquals(3, top.size()); - assertEquals(player2, top.get(0).playerId()); - assertEquals(player3, top.get(1).playerId()); - assertEquals(player1, top.get(2).playerId()); - } - - @Test - void testGetTopByMineFiltersZeroBlocks() { - UUID player1 = UUID.randomUUID(); - UUID player2 = UUID.randomUUID(); - - statsManager.getOrCreate(player1).setMineBlocks("mine1", 100); - statsManager.getOrCreate(player2).setMineBlocks("mine1", 0); - - List top = leaderboard.getTopByMine("mine1", 10); - assertEquals(1, top.size(), "Should filter out players with 0 blocks"); - assertEquals(player1, top.get(0).playerId()); - } - - @Test - void testGetTopByMineOnlyIncludesSpecificMine() { - UUID player1 = UUID.randomUUID(); - UUID player2 = UUID.randomUUID(); - - statsManager.getOrCreate(player1).setMineBlocks("mine1", 100); - statsManager.getOrCreate(player2).setMineBlocks("mine2", 200); - - List top = leaderboard.getTopByMine("mine1", 10); - assertEquals(1, top.size(), "Should only include players with blocks in specified mine"); - assertEquals(player1, top.get(0).playerId()); - } -} diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardTest.java index 4e571e1..00376c9 100644 --- a/src/test/java/dev/loki/lomines/data/LeaderboardTest.java +++ b/src/test/java/dev/loki/lomines/data/LeaderboardTest.java @@ -1,10 +1,7 @@ package dev.loki.lomines.data; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.model.Leaderboard; -import dev.loki.lomines.data.stats.model.LeaderboardEntry; -import dev.loki.lomines.data.stats.model.PlayerStats; -import dev.loki.lomines.data.stats.service.StatsManager; +import dev.lolib.core.LoLogger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,34 +27,36 @@ class LeaderboardTest { void setUp() { // Create a mock plugin LoMinesPlugin mockPlugin = mock(LoMinesPlugin.class); + LoLogger loLogger = mock(LoLogger.class); File mockDataFolder = new File(System.getProperty("java.io.tmpdir"), "lomines-test"); when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder); when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger")); + when(mockPlugin.loLogger()).thenReturn(loLogger); statsManager = new StatsManager(mockPlugin); leaderboard = statsManager.getLeaderboard(); } @Test - void testGetTopTotalEmptyStats() { + void testGetTopTotal_EmptyStats() { List top = leaderboard.getTopTotal(10); assertTrue(top.isEmpty(), "Top list should be empty when no stats exist"); } @Test - void testGetTopTotalSinglePlayer() { + void testGetTopTotal_SinglePlayer() { UUID playerId = UUID.randomUUID(); PlayerStats stats = statsManager.getOrCreate(playerId); stats.setTotalBlocks(100); List top = leaderboard.getTopTotal(10); assertEquals(1, top.size()); - assertEquals(playerId, top.get(0).playerId()); - assertEquals(100, top.get(0).count()); + assertEquals(playerId, top.get(0).getPlayerId()); + assertEquals(100, top.get(0).getCount()); } @Test - void testGetTopTotalSortedDescending() { + void testGetTopTotal_SortedDescending() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); UUID player3 = UUID.randomUUID(); @@ -68,13 +67,13 @@ class LeaderboardTest { List top = leaderboard.getTopTotal(10); assertEquals(3, top.size()); - assertEquals(player2, top.get(0).playerId()); // 200 blocks - assertEquals(player3, top.get(1).playerId()); // 100 blocks - assertEquals(player1, top.get(2).playerId()); // 50 blocks + assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks + assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks + assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks } @Test - void testGetTopTotalLimitRespected() { + void testGetTopTotal_LimitRespected() { for (int i = 0; i < 10; i++) { UUID playerId = UUID.randomUUID(); statsManager.getOrCreate(playerId).setTotalBlocks(i + 1); @@ -85,7 +84,7 @@ class LeaderboardTest { } @Test - void testGetTopTotalFiltersZeroBlocks() { + void testGetTopTotal_FiltersZeroBlocks() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); @@ -94,11 +93,11 @@ class LeaderboardTest { List top = leaderboard.getTopTotal(10); assertEquals(1, top.size(), "Should filter out players with 0 blocks"); - assertEquals(player1, top.get(0).playerId()); + assertEquals(player1, top.get(0).getPlayerId()); } @Test - void testGetTopTotalCachingWorks() { + void testGetTopTotal_CachingWorks() { UUID playerId = UUID.randomUUID(); statsManager.getOrCreate(playerId).setTotalBlocks(100); @@ -109,11 +108,11 @@ class LeaderboardTest { List top2 = leaderboard.getTopTotal(10); assertEquals(top1.size(), top2.size()); - assertEquals(top1.get(0).playerId(), top2.get(0).playerId()); + assertEquals(top1.get(0).getPlayerId(), top2.get(0).getPlayerId()); } @Test - void testInvalidateCacheClearsCache() { + void testInvalidateCache_ClearsCache() { UUID playerId = UUID.randomUUID(); statsManager.getOrCreate(playerId).setTotalBlocks(100); @@ -130,7 +129,78 @@ class LeaderboardTest { // Should rebuild cache with new player List top = leaderboard.getTopTotal(10); assertEquals(2, top.size(), "Cache should be rebuilt after invalidation"); - assertEquals(player2, top.get(0).playerId(), "New player should be first"); + assertEquals(player2, top.get(0).getPlayerId(), "New player should be first"); } + @Test + void testGetTopByMine_SortedDescending() { + UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); + UUID player3 = UUID.randomUUID(); + + statsManager.getOrCreate(player1).setMineBlocks("mine1", 50); + statsManager.getOrCreate(player2).setMineBlocks("mine1", 200); + statsManager.getOrCreate(player3).setMineBlocks("mine1", 100); + + List top = leaderboard.getTopByMine("mine1", 10); + assertEquals(3, top.size()); + assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks + assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks + assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks + } + + @Test + void testGetTopByMine_FiltersZeroBlocks() { + UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); + + statsManager.getOrCreate(player1).setMineBlocks("mine1", 100); + statsManager.getOrCreate(player2).setMineBlocks("mine1", 0); + + List top = leaderboard.getTopByMine("mine1", 10); + assertEquals(1, top.size(), "Should filter out players with 0 blocks"); + assertEquals(player1, top.get(0).getPlayerId()); + } + + @Test + void testGetTopByMine_OnlyIncludesSpecificMine() { + UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); + + statsManager.getOrCreate(player1).setMineBlocks("mine1", 100); + statsManager.getOrCreate(player2).setMineBlocks("mine2", 200); + + List top = leaderboard.getTopByMine("mine1", 10); + assertEquals(1, top.size(), "Should only include players with blocks in specified mine"); + assertEquals(player1, top.get(0).getPlayerId()); + } + + @Test + void testGetPosition_ReturnsCorrectPosition() { + UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); + UUID player3 = UUID.randomUUID(); + + statsManager.getOrCreate(player1).setTotalBlocks(50); + statsManager.getOrCreate(player2).setTotalBlocks(200); + statsManager.getOrCreate(player3).setTotalBlocks(100); + + assertEquals(1, leaderboard.getPosition(player2), "Player with most blocks should be position 1"); + assertEquals(2, leaderboard.getPosition(player3), "Player with second most blocks should be position 2"); + assertEquals(3, leaderboard.getPosition(player1), "Player with least blocks should be position 3"); + } + + @Test + void testGetPosition_ReturnsMinusOneForNonExistentPlayer() { + UUID playerId = UUID.randomUUID(); + assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for non-existent player"); + } + + @Test + void testGetPosition_ReturnsMinusOneForZeroBlocks() { + UUID playerId = UUID.randomUUID(); + statsManager.getOrCreate(playerId).setTotalBlocks(0); + + assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for player with 0 blocks"); + } } diff --git a/src/test/java/dev/loki/lomines/data/StatsManagerConcurrencyTest.java b/src/test/java/dev/loki/lomines/data/StatsManagerConcurrencyTest.java deleted file mode 100644 index 7ee8246..0000000 --- a/src/test/java/dev/loki/lomines/data/StatsManagerConcurrencyTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package dev.loki.lomines.data; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.service.StatsManager; -import org.bukkit.Bukkit; -import org.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.MockedStatic; - -import java.nio.file.Path; -import java.util.UUID; -import java.util.logging.Logger; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class StatsManagerConcurrencyTest { - - @TempDir - Path tempDir; - private StatsManager statsManager; - private LoMinesPlugin plugin; - private BukkitScheduler scheduler; - - @BeforeEach - void setUp() { - plugin = mock(LoMinesPlugin.class); - scheduler = mock(BukkitScheduler.class); - - when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); - when(plugin.getLogger()).thenReturn(Logger.getLogger("TestLogger")); - - statsManager = new StatsManager(plugin); - } - - @Test - void testStartAutoSave() { - try (MockedStatic schedulerStatic = mockStatic(Bukkit.class)) { - BukkitTask task = mock(BukkitTask.class); - - schedulerStatic.when(Bukkit::getScheduler).thenReturn(scheduler); - when(scheduler.runTaskTimerAsynchronously(any(), any(Runnable.class), anyLong(), anyLong())).thenReturn(task); - - statsManager.startAutoSave(); - - verify(scheduler).runTaskTimerAsynchronously(any(), any(Runnable.class), eq(6000L), eq(6000L)); - } - } - - @Test - void testStopAutoSave() { - try (MockedStatic schedulerStatic = mockStatic(Bukkit.class)) { - BukkitTask task = mock(BukkitTask.class); - - schedulerStatic.when(Bukkit::getScheduler).thenReturn(scheduler); - when(scheduler.runTaskTimerAsynchronously(any(), any(Runnable.class), anyLong(), anyLong())).thenReturn(task); - - statsManager.startAutoSave(); - statsManager.stopAutoSave(); - - verify(task).cancel(); - } - } - - @Test - void testStopAutoSaveWithoutStart() { - assertDoesNotThrow(() -> statsManager.stopAutoSave()); - } - - @Test - void testGetAllStats() { - UUID player1 = UUID.randomUUID(); - UUID player2 = UUID.randomUUID(); - - statsManager.incrementBlocks(player1, "Mine1"); - statsManager.incrementBlocks(player2, "Mine2"); - - assertEquals(2, statsManager.getAllStats().size()); - } - - @Test - void testGetLeaderboard() { - assertNotNull(statsManager.getLeaderboard()); - } -} diff --git a/src/test/java/dev/loki/lomines/data/StatsManagerIOTest.java b/src/test/java/dev/loki/lomines/data/StatsManagerIOTest.java deleted file mode 100644 index 1cf4a8d..0000000 --- a/src/test/java/dev/loki/lomines/data/StatsManagerIOTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package dev.loki.lomines.data; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.model.PlayerStats; -import dev.loki.lomines.data.stats.service.StatsManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.UUID; -import java.util.logging.Logger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class StatsManagerIOTest { - - @TempDir - Path tempDir; - private StatsManager statsManager; - private LoMinesPlugin plugin; - - @BeforeEach - void setUp() { - plugin = mock(LoMinesPlugin.class); - - when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); - when(plugin.getLogger()).thenReturn(Logger.getLogger("TestLogger")); - - statsManager = new StatsManager(plugin); - } - - @Test - void testLoadNonExistentFile() throws IOException { - statsManager.load(); - - assertTrue(statsManager.getAllStats().isEmpty()); - } - - @Test - void testLoadEmptyFile() throws IOException { - Path statsFile = tempDir.resolve("stats.yml"); - Files.writeString(statsFile, ""); - - statsManager.load(); - - assertTrue(statsManager.getAllStats().isEmpty()); - } - - @Test - void testLoadValidStats() throws IOException { - UUID playerId = UUID.randomUUID(); - String yamlContent = String.format(""" - players: - %s: - total: 100 - mines: - mine1: 60 - mine2: 40 - """, playerId); - - Path statsFile = tempDir.resolve("stats.yml"); - Files.writeString(statsFile, yamlContent); - - statsManager.load(); - - PlayerStats stats = statsManager.getOrCreate(playerId); - assertEquals(100, stats.getTotalBlocks()); - assertEquals(60, stats.getMineBlocks("mine1")); - assertEquals(40, stats.getMineBlocks("mine2")); - } - - @Test - void testLoadInvalidUUID() throws IOException { - String yamlContent = """ - players: - invalid-uuid: - total: 100 - """; - - Path statsFile = tempDir.resolve("stats.yml"); - Files.writeString(statsFile, yamlContent); - - statsManager.load(); - - assertTrue(statsManager.getAllStats().isEmpty()); - } - - @Test - void testSaveAndLoadRoundTrip() throws IOException { - UUID player1 = UUID.randomUUID(); - UUID player2 = UUID.randomUUID(); - - statsManager.incrementBlocks(player1, "Mine1"); - statsManager.incrementBlocks(player1, "Mine1"); - statsManager.incrementBlocks(player1, "Mine2"); - statsManager.incrementBlocks(player2, "Mine1"); - - statsManager.save(); - - StatsManager newManager = new StatsManager(plugin); - newManager.load(); - - PlayerStats stats1 = newManager.getOrCreate(player1); - assertEquals(3, stats1.getTotalBlocks()); - assertEquals(2, stats1.getMineBlocks("Mine1")); - assertEquals(1, stats1.getMineBlocks("Mine2")); - - PlayerStats stats2 = newManager.getOrCreate(player2); - assertEquals(1, stats2.getTotalBlocks()); - assertEquals(1, stats2.getMineBlocks("Mine1")); - } -} diff --git a/src/test/java/dev/loki/lomines/data/StatsManagerTest.java b/src/test/java/dev/loki/lomines/data/StatsManagerTest.java index bc1b749..5429cc5 100644 --- a/src/test/java/dev/loki/lomines/data/StatsManagerTest.java +++ b/src/test/java/dev/loki/lomines/data/StatsManagerTest.java @@ -1,13 +1,14 @@ package dev.loki.lomines.data; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.data.stats.model.PlayerStats; -import dev.loki.lomines.data.stats.service.StatsManager; - +import dev.lolib.core.LoLogger; +import dev.lolib.scheduler.ScheduledTask; +import dev.lolib.scheduler.Scheduler; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; import java.io.IOException; import java.nio.file.Files; @@ -15,13 +16,9 @@ import java.nio.file.Path; import java.util.UUID; import java.util.logging.Logger; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; /** * Unit tests for StatsManager. @@ -33,13 +30,17 @@ class StatsManagerTest { Path tempDir; private StatsManager statsManager; private LoMinesPlugin plugin; - + private Scheduler scheduler; @BeforeEach void setUp() { plugin = mock(LoMinesPlugin.class); + LoLogger loLogger = mock(LoLogger.class); + scheduler = mock(Scheduler.class); + when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); when(plugin.getLogger()).thenReturn(Logger.getLogger("TestLogger")); + when(plugin.loLogger()).thenReturn(loLogger); statsManager = new StatsManager(plugin); } @@ -119,4 +120,141 @@ class StatsManagerTest { assertEquals(2, yaml.getLong(playerPath + ".mines.testmine")); assertEquals(1, yaml.getLong(playerPath + ".mines.anothermine")); } + + @Test + void testLoadNonExistentFile() throws IOException { + // Should not throw exception, just log info + statsManager.load(); + + // Stats should be empty + assertTrue(statsManager.getAllStats().isEmpty()); + } + + @Test + void testLoadEmptyFile() throws IOException { + Path statsFile = tempDir.resolve("stats.yml"); + Files.writeString(statsFile, ""); + + statsManager.load(); + + assertTrue(statsManager.getAllStats().isEmpty()); + } + + @Test + void testLoadValidStats() throws IOException { + UUID playerId = UUID.randomUUID(); + String yamlContent = String.format(""" + players: + %s: + total: 100 + mines: + mine1: 60 + mine2: 40 + """, playerId); + + Path statsFile = tempDir.resolve("stats.yml"); + Files.writeString(statsFile, yamlContent); + + statsManager.load(); + + PlayerStats stats = statsManager.getOrCreate(playerId); + assertEquals(100, stats.getTotalBlocks()); + assertEquals(60, stats.getMineBlocks("mine1")); + assertEquals(40, stats.getMineBlocks("mine2")); + } + + @Test + void testLoadInvalidUUID() throws IOException { + String yamlContent = """ + players: + invalid-uuid: + total: 100 + """; + + Path statsFile = tempDir.resolve("stats.yml"); + Files.writeString(statsFile, yamlContent); + + // Should not throw exception, just log warning + statsManager.load(); + + assertTrue(statsManager.getAllStats().isEmpty()); + } + + @Test + void testSaveAndLoadRoundTrip() throws IOException { + UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); + + statsManager.incrementBlocks(player1, "Mine1"); + statsManager.incrementBlocks(player1, "Mine1"); + statsManager.incrementBlocks(player1, "Mine2"); + statsManager.incrementBlocks(player2, "Mine1"); + + statsManager.save(); + + // Create new manager and load + StatsManager newManager = new StatsManager(plugin); + newManager.load(); + + PlayerStats stats1 = newManager.getOrCreate(player1); + assertEquals(3, stats1.getTotalBlocks()); + assertEquals(2, stats1.getMineBlocks("Mine1")); + assertEquals(1, stats1.getMineBlocks("Mine2")); + + PlayerStats stats2 = newManager.getOrCreate(player2); + assertEquals(1, stats2.getTotalBlocks()); + assertEquals(1, stats2.getMineBlocks("Mine1")); + } + + @Test + void testStartAutoSave() { + try (MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + ScheduledTask task = mock(ScheduledTask.class); + + schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler); + when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task); + + statsManager.startAutoSave(); + + // Verify that runTimerAsync was called with correct interval (5 minutes = 6000 ticks) + verify(scheduler).runTimerAsync(any(Runnable.class), eq(6000L), eq(6000L)); + } + } + + @Test + void testStopAutoSave() { + try (MockedStatic schedulerStatic = mockStatic(Scheduler.class)) { + ScheduledTask task = mock(ScheduledTask.class); + + schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler); + when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task); + + statsManager.startAutoSave(); + statsManager.stopAutoSave(); + + verify(task).cancel(); + } + } + + @Test + void testStopAutoSaveWithoutStart() { + // Should not throw exception + assertDoesNotThrow(() -> statsManager.stopAutoSave()); + } + + @Test + void testGetAllStats() { + UUID player1 = UUID.randomUUID(); + UUID player2 = UUID.randomUUID(); + + statsManager.incrementBlocks(player1, "Mine1"); + statsManager.incrementBlocks(player2, "Mine2"); + + assertEquals(2, statsManager.getAllStats().size()); + } + + @Test + void testGetLeaderboard() { + assertNotNull(statsManager.getLeaderboard()); + } } diff --git a/src/test/java/dev/loki/lomines/data/config/MineConfigTest.java b/src/test/java/dev/loki/lomines/data/config/MineConfigTest.java index 93c3d40..0e3656f 100644 --- a/src/test/java/dev/loki/lomines/data/config/MineConfigTest.java +++ b/src/test/java/dev/loki/lomines/data/config/MineConfigTest.java @@ -1,7 +1,5 @@ package dev.loki.lomines.data.config; -import dev.loki.lomines.data.config.model.MineConfig; -import dev.loki.lomines.data.config.model.MineConfigDefaults; import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.FillMode; @@ -10,111 +8,184 @@ import dev.loki.lomines.data.config.reset.ResetConfig; import dev.loki.lomines.data.config.reward.RewardConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.data.config.ui.UIConfig; +import dev.loki.lomines.util.location.Cuboid; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; class MineConfigTest { - private ServerMock server; - private World world; - - @BeforeEach - void setUp() { - server = MockBukkit.mock(); - server.addSimpleWorld("world"); - world = server.getWorld("world"); - } - - @AfterEach - void tearDown() { - MockBukkit.unmock(); - } - - private RegionConfig testRegion() { - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 10, 74, 10); - return RegionConfig.fromSelections(List.of(loc1, loc2)); - } - - private BlockConfig testBlocks() { - Map weights = new HashMap<>(); - weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); - return new BlockConfig(weights, FillMode.CUBOID, null); - } - @Test void testBuilder() { - RegionConfig region = testRegion(); - BlockConfig blocks = testBlocks(); + try (MockedStatic bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world); - MineConfig config = MineConfig.builder("testmine") - .region(region) - .blocks(blocks) - .reset(ResetConfig.defaults()) - .rewards(RewardConfig.empty()) - .teleport(TeleportConfig.disabled()) - .ui(UIConfig.defaults()) - .build(); + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 10, 74, 10); + RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); - assertEquals("testmine", config.name()); - assertEquals(1, config.region().regionCount()); - assertEquals(1, config.blocks().blockCount()); + Map weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + MineConfig config = MineConfig.builder("testmine") + .region(region) + .blocks(blocks) + .reset(ResetConfig.defaults()) + .rewards(RewardConfig.empty()) + .teleport(TeleportConfig.disabled()) + .ui(UIConfig.defaults()) + .build(); + + assertEquals("testmine", config.name()); + assertEquals(1, config.region().regionCount()); + assertEquals(1, config.blocks().blockCount()); + } } @Test void testDefaultsFactory() { - RegionConfig region = testRegion(); - BlockConfig blocks = testBlocks(); + try (MockedStatic bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world); - MineConfig config = MineConfigDefaults.defaults("mymine", region, blocks); + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 10, 74, 10); + RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); - assertEquals("mymine", config.name()); - assertNotNull(config.reset()); - assertNotNull(config.rewards()); - assertNotNull(config.teleport()); - assertNotNull(config.ui()); + Map weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + MineConfig config = MineConfig.defaults("mymine", region, blocks); + + assertEquals("mymine", config.name()); + assertNotNull(config.reset()); + assertNotNull(config.rewards()); + assertNotNull(config.teleport()); + assertNotNull(config.ui()); + } } @Test void testNameNormalized() { - RegionConfig region = testRegion(); - BlockConfig blocks = testBlocks(); + try (MockedStatic bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world); - MineConfig config = MineConfig.builder("TestMine_123 ") - .region(region) - .blocks(blocks) - .build(); + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 10, 74, 10); + RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); - assertEquals("testmine_123", config.name()); + Map weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + MineConfig config = MineConfig.builder("TestMine_123 ") + .region(region) + .blocks(blocks) + .build(); + + assertEquals("testmine_123", config.name()); + } + } + + @Test + void testNullNameThrows() { + assertThrows(NullPointerException.class, () -> + MineConfig.builder(null)); + } + + @Test + void testBlankNameThrows() { + try (MockedStatic bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world); + + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 10, 74, 10); + RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); + + Map weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + assertThrows(IllegalArgumentException.class, () -> + MineConfig.builder(" ") + .region(region) + .blocks(blocks) + .build()); + } + } + + @Test + void testNullRegionThrows() { + Map weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + assertThrows(NullPointerException.class, () -> + MineConfig.builder("test") + .region(null) + .blocks(blocks) + .build()); + } + + @Test + void testNullBlocksThrows() { + try (MockedStatic bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) { + World world = mock(World.class); + when(world.getName()).thenReturn("world"); + bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world); + + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 10, 74, 10); + RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); + + assertThrows(NullPointerException.class, () -> + MineConfig.builder("test") + .region(region) + .blocks(null) + .build()); + } } @Test void testWorldNameDelegation() { - server.addSimpleWorld("nether"); - World nether = server.getWorld("nether"); - Location loc1 = new Location(nether, 0, 64, 0); - Location loc2 = new Location(nether, 10, 74, 10); - RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); - BlockConfig blocks = testBlocks(); + try (MockedStatic bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) { + World world = mock(World.class); + when(world.getName()).thenReturn("nether"); + bukkit.when(() -> org.bukkit.Bukkit.getWorld("nether")).thenReturn(world); - MineConfig config = MineConfig.builder("test") - .region(region) - .blocks(blocks) - .build(); + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 10, 74, 10); + RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); - assertEquals("nether", config.worldName()); + Map weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + MineConfig config = MineConfig.builder("test") + .region(region) + .blocks(blocks) + .build(); + + assertEquals("nether", config.worldName()); + } } } diff --git a/src/test/java/dev/loki/lomines/data/config/MineConfigValidationTest.java b/src/test/java/dev/loki/lomines/data/config/MineConfigValidationTest.java deleted file mode 100644 index 26d8a3c..0000000 --- a/src/test/java/dev/loki/lomines/data/config/MineConfigValidationTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package dev.loki.lomines.data.config; - -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 org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -class MineConfigValidationTest { - - private ServerMock server; - private World world; - - @BeforeEach - void setUp() { - server = MockBukkit.mock(); - server.addSimpleWorld("world"); - world = server.getWorld("world"); - } - - @AfterEach - void tearDown() { - MockBukkit.unmock(); - } - - private RegionConfig testRegion() { - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 10, 74, 10); - return RegionConfig.fromSelections(List.of(loc1, loc2)); - } - - private BlockConfig testBlocks() { - Map weights = new HashMap<>(); - weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); - return new BlockConfig(weights, FillMode.CUBOID, null); - } - - @Test - void testNullNameThrows() { - assertThrows(NullPointerException.class, () -> - MineConfig.builder(null).build()); - } - - @Test - void testBlankNameThrows() { - RegionConfig region = testRegion(); - BlockConfig blocks = testBlocks(); - - assertThrows(IllegalArgumentException.class, () -> - MineConfig.builder(" ") - .region(region) - .blocks(blocks) - .build()); - } - - @Test - void testNullRegionThrows() { - BlockConfig blocks = testBlocks(); - - assertThrows(NullPointerException.class, () -> - MineConfig.builder("test") - .region(null) - .blocks(blocks) - .build()); - } - - @Test - void testNullBlocksThrows() { - RegionConfig region = testRegion(); - - assertThrows(NullPointerException.class, () -> - MineConfig.builder("test") - .region(region) - .blocks(null) - .build()); - } -} diff --git a/src/test/java/dev/loki/lomines/data/config/block/BlockConfigTest.java b/src/test/java/dev/loki/lomines/data/config/block/BlockConfigTest.java index 48d0565..fe6e013 100644 --- a/src/test/java/dev/loki/lomines/data/config/block/BlockConfigTest.java +++ b/src/test/java/dev/loki/lomines/data/config/block/BlockConfigTest.java @@ -1,35 +1,15 @@ package dev.loki.lomines.data.config.block; import org.bukkit.Material; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; import java.util.HashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class BlockConfigTest { - private ServerMock server; - - @BeforeEach - void setUp() { - server = MockBukkit.mock(); - } - - @AfterEach - void tearDown() { - MockBukkit.unmock(); - } - @Test void testValidConfig() { Map weights = new HashMap<>(); diff --git a/src/test/java/dev/loki/lomines/data/config/block/BlockKeyTest.java b/src/test/java/dev/loki/lomines/data/config/block/BlockKeyTest.java index ebf4754..f96f331 100644 --- a/src/test/java/dev/loki/lomines/data/config/block/BlockKeyTest.java +++ b/src/test/java/dev/loki/lomines/data/config/block/BlockKeyTest.java @@ -1,31 +1,12 @@ package dev.loki.lomines.data.config.block; import org.bukkit.Material; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockbukkit.mockbukkit.MockBukkit; -import org.mockbukkit.mockbukkit.ServerMock; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; class BlockKeyTest { - private ServerMock server; - - @BeforeEach - void setUp() { - server = MockBukkit.mock(); - } - - @AfterEach - void tearDown() { - MockBukkit.unmock(); - } - @Test void testVanillaSerialization() { BlockKey.Vanilla stone = new BlockKey.Vanilla(Material.STONE); diff --git a/src/test/java/dev/loki/lomines/data/config/reset/ResetConfigTest.java b/src/test/java/dev/loki/lomines/data/config/reset/ResetConfigTest.java index 953f40d..53f9e90 100644 --- a/src/test/java/dev/loki/lomines/data/config/reset/ResetConfigTest.java +++ b/src/test/java/dev/loki/lomines/data/config/reset/ResetConfigTest.java @@ -5,9 +5,7 @@ import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class ResetConfigTest { @@ -57,10 +55,115 @@ class ResetConfigTest { assertEquals(Duration.ofDays(1), ResetConfig.builder().interval("1d").build().interval()); } + @Test + void testInvalidIntervalDefaultsToFiveMinutes() { + ResetConfig config = ResetConfig.builder() + .interval("invalid") + .build(); + + assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval()); + } + + @Test + void testNegativeIntervalDefaults() { + ResetConfig config = new ResetConfig( + Duration.ofSeconds(-10), + 10.0, + false, + List.of(), + "" + ); + + assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval()); + } + + @Test + void testZeroIntervalDefaults() { + ResetConfig config = new ResetConfig( + Duration.ZERO, + 10.0, + false, + List.of(), + "" + ); + + assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval()); + } + + @Test + void testTooShortIntervalClamped() { + ResetConfig config = ResetConfig.builder() + .interval("0.5s") + .build(); + + assertEquals(Duration.ofSeconds(1), config.interval()); + } + + @Test + void testTooLongIntervalClamped() { + ResetConfig config = ResetConfig.builder() + .interval("48h") + .build(); + + assertEquals(Duration.ofHours(24), config.interval()); + } + + @Test + void testInvalidPercentTriggerClamped() { + ResetConfig negative = new ResetConfig( + Duration.ofMinutes(5), + -10.0, + false, + List.of(), + "" + ); + assertEquals(ResetConfig.DEFAULT_PERCENT_TRIGGER, negative.percentTrigger()); + + ResetConfig over100 = new ResetConfig( + Duration.ofMinutes(5), + 150.0, + false, + List.of(), + "" + ); + assertEquals(ResetConfig.DEFAULT_PERCENT_TRIGGER, over100.percentTrigger()); + } + @Test void testIntervalDisplay() { assertEquals("30s", new ResetConfig(Duration.ofSeconds(30), 10.0, false, List.of(), "").intervalDisplay()); assertEquals("5m", new ResetConfig(Duration.ofMinutes(5), 10.0, false, List.of(), "").intervalDisplay()); assertEquals("2h", new ResetConfig(Duration.ofHours(2), 10.0, false, List.of(), "").intervalDisplay()); } + + @Test + void testPercentTriggerEnabled() { + assertFalse(ResetConfig.defaults().isPercentTriggerEnabled()); + assertFalse(new ResetConfig(Duration.ofMinutes(5), 10.0, false, List.of(), "").isPercentTriggerEnabled()); + assertTrue(new ResetConfig(Duration.ofMinutes(5), 10.0, true, List.of(), "").isPercentTriggerEnabled()); + assertFalse(new ResetConfig(Duration.ofMinutes(5), 0.0, true, List.of(), "").isPercentTriggerEnabled()); + } + + @Test + void testImmutableCommands() { + ResetConfig config = ResetConfig.builder() + .commands(List.of("cmd1")) + .build(); + + assertThrows(UnsupportedOperationException.class, () -> + config.commands().add("cmd2")); + } + + @Test + void testNullCommandsDefaultsToEmpty() { + ResetConfig config = new ResetConfig( + Duration.ofMinutes(5), + 10.0, + false, + null, + "test" + ); + + assertTrue(config.commands().isEmpty()); + } } diff --git a/src/test/java/dev/loki/lomines/data/config/reset/ResetConfigValidationTest.java b/src/test/java/dev/loki/lomines/data/config/reset/ResetConfigValidationTest.java deleted file mode 100644 index f335248..0000000 --- a/src/test/java/dev/loki/lomines/data/config/reset/ResetConfigValidationTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package dev.loki.lomines.data.config.reset; - -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class ResetConfigValidationTest { - - @Test - void testInvalidIntervalDefaultsToFiveMinutes() { - ResetConfig config = ResetConfig.builder() - .interval("invalid") - .build(); - - assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval()); - } - - @Test - void testNegativeIntervalDefaults() { - ResetConfig config = new ResetConfig( - Duration.ofSeconds(-10), - 10.0, - false, - List.of(), - "" - ); - - assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval()); - } - - @Test - void testZeroIntervalDefaults() { - ResetConfig config = new ResetConfig( - Duration.ZERO, - 10.0, - false, - List.of(), - "" - ); - - assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval()); - } - - @Test - void testTooShortIntervalClamped() { - ResetConfig config = ResetConfig.builder() - .interval(Duration.ofMillis(500)) - .build(); - - assertEquals(Duration.ofSeconds(1), config.interval()); - } - - @Test - void testTooLongIntervalClamped() { - ResetConfig config = ResetConfig.builder() - .interval("48h") - .build(); - - assertEquals(Duration.ofHours(24), config.interval()); - } - - @Test - void testInvalidPercentTriggerClamped() { - ResetConfig negative = new ResetConfig( - Duration.ofMinutes(5), - -10.0, - false, - List.of(), - "" - ); - assertEquals(ResetConfig.DEFAULT_PERCENT_TRIGGER, negative.percentTrigger()); - - ResetConfig over100 = new ResetConfig( - Duration.ofMinutes(5), - 150.0, - false, - List.of(), - "" - ); - assertEquals(ResetConfig.DEFAULT_PERCENT_TRIGGER, over100.percentTrigger()); - } - - @Test - void testPercentTriggerEnabled() { - assertFalse(ResetConfig.defaults().isPercentTriggerEnabled()); - assertFalse(new ResetConfig(Duration.ofMinutes(5), 10.0, false, List.of(), "").isPercentTriggerEnabled()); - assertTrue(new ResetConfig(Duration.ofMinutes(5), 10.0, true, List.of(), "").isPercentTriggerEnabled()); - assertFalse(new ResetConfig(Duration.ofMinutes(5), 0.0, true, List.of(), "").isPercentTriggerEnabled()); - } - - @Test - void testImmutableCommands() { - ResetConfig config = ResetConfig.builder() - .commands(List.of("cmd1")) - .build(); - - assertThrows(UnsupportedOperationException.class, () -> - config.commands().add("cmd2")); - } - - @Test - void testNullCommandsDefaultsToEmpty() { - ResetConfig config = new ResetConfig( - Duration.ofMinutes(5), - 10.0, - false, - null, - "test" - ); - - assertTrue(config.commands().isEmpty()); - } -} diff --git a/src/test/java/dev/loki/lomines/data/config/ui/UIConfigTest.java b/src/test/java/dev/loki/lomines/data/config/ui/UIConfigTest.java index 5ac6776..ac216cf 100644 --- a/src/test/java/dev/loki/lomines/data/config/ui/UIConfigTest.java +++ b/src/test/java/dev/loki/lomines/data/config/ui/UIConfigTest.java @@ -4,9 +4,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class UIConfigTest { @@ -25,8 +23,8 @@ class UIConfigTest { UIConfig config = UIConfig.disabled(); assertFalse(config.actionBarEnabled()); - assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat()); - assertEquals(1.0, config.actionBarRange()); + assertEquals("", config.actionBarFormat()); + assertEquals(0, config.actionBarRange()); } @Test @@ -35,8 +33,7 @@ class UIConfigTest { true, "{mine}", 100.0, - "HH:mm:ss", - null + "HH:mm:ss" ); assertTrue(config.actionBarEnabled()); @@ -47,7 +44,7 @@ class UIConfigTest { @Test void testNullFormatDefaults() { - UIConfig config = new UIConfig(true, null, 50.0, null, null); + UIConfig config = new UIConfig(true, null, 50.0, null); assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat()); assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat()); @@ -55,7 +52,7 @@ class UIConfigTest { @Test void testBlankFormatDefaults() { - UIConfig config = new UIConfig(true, " ", 50.0, " ", null); + UIConfig config = new UIConfig(true, " ", 50.0, " "); assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat()); assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat()); @@ -63,14 +60,14 @@ class UIConfigTest { @Test void testNegativeRangeClamped() { - UIConfig config = new UIConfig(true, "test", -10.0, "mm:ss", null); + UIConfig config = new UIConfig(true, "test", -10.0, "mm:ss"); assertEquals(1.0, config.actionBarRange()); } @Test void testRangeSquared() { - UIConfig config = new UIConfig(true, "test", 50.0, "mm:ss", null); + UIConfig config = new UIConfig(true, "test", 50.0, "mm:ss"); assertEquals(2500.0, config.actionBarRangeSquared(), 0.001); } @@ -88,7 +85,7 @@ class UIConfigTest { @Test void testFormatTimerMmSs() { - UIConfig config = new UIConfig(true, "", 0, "mm:ss", null); + UIConfig config = new UIConfig(true, "", 0, "mm:ss"); assertEquals("00:30", config.formatTimer(30)); assertEquals("05:00", config.formatTimer(300)); @@ -97,7 +94,7 @@ class UIConfigTest { @Test void testFormatTimerHhMmSs() { - UIConfig config = new UIConfig(true, "", 0, "HH:mm:ss", null); + UIConfig config = new UIConfig(true, "", 0, "HH:mm:ss"); assertEquals("0:00:30", config.formatTimer(30)); assertEquals("0:05:00", config.formatTimer(300)); diff --git a/src/test/java/dev/loki/lomines/util/ChunkUtilsRefreshTest.java b/src/test/java/dev/loki/lomines/util/ChunkUtilsRefreshTest.java deleted file mode 100644 index c6d1a43..0000000 --- a/src/test/java/dev/loki/lomines/util/ChunkUtilsRefreshTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package dev.loki.lomines.util; - -import dev.loki.lomines.util.format.ChunkUtils; -import dev.loki.lomines.util.location.geo.Cuboid; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class ChunkUtilsRefreshTest { - - @Mock - private World world; - - @Mock - private Chunk chunk; - - @Mock - private Player player; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - void testUpdateChunksWithLargeRegion() { - Location loc1 = new Location(world, 0, 0, 0); - Location loc2 = new Location(world, 100, 100, 100); - when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); - when(world.getPlayers()).thenReturn(Collections.emptyList()); - when(chunk.getWorld()).thenReturn(world); - - Cuboid region = new Cuboid(loc1, loc2); - assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); - - verify(world, atLeast(40)).getChunkAt(anyInt(), anyInt()); - } - - @Test - void testUpdateChunksWithNoPlayers() { - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 5, 70, 5); - when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); - when(world.getPlayers()).thenReturn(Collections.emptyList()); - when(chunk.getWorld()).thenReturn(world); - - Cuboid region = new Cuboid(loc1, loc2); - assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); - } - - @Test - void testChunkCoordinateCalculation() { - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 15, 70, 15); - when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); - when(world.getPlayers()).thenReturn(Collections.emptyList()); - when(chunk.getWorld()).thenReturn(world); - - Cuboid singleChunk = new Cuboid(loc1, loc2); - ChunkUtils.updateChunks(singleChunk); - - verify(world, times(1)).getChunkAt(0, 0); - } -} diff --git a/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java b/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java index e62e85d..704c48c 100644 --- a/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java +++ b/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java @@ -1,7 +1,5 @@ package dev.loki.lomines.util; -import dev.loki.lomines.util.format.ChunkUtils; -import dev.loki.lomines.util.location.geo.Cuboid; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.World; @@ -16,10 +14,7 @@ import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * Unit tests for ChunkUtils. @@ -42,17 +37,19 @@ class ChunkUtilsTest { } @Test - void testUpdateChunksWithNullRegionThrowsException() { + void testUpdateChunks_withNullRegion_throwsException() { assertThrows(IllegalArgumentException.class, () -> { ChunkUtils.updateChunks(null); }); } @Test - void testUpdateChunksWithSingleChunkRegion() { + void testUpdateChunks_withSingleChunkRegion() { // Create a small region within a single chunk Location loc1 = new Location(world, 0, 64, 0); Location loc2 = new Location(world, 5, 70, 5); + Cuboid region = new Cuboid(loc1, loc2); + when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getPlayers()).thenReturn(Collections.singletonList(player)); when(chunk.getWorld()).thenReturn(world); @@ -60,7 +57,6 @@ class ChunkUtilsTest { when(chunk.getZ()).thenReturn(0); // Should not throw exception - Cuboid region = new Cuboid(loc1, loc2); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); // Verify chunk was retrieved @@ -68,17 +64,18 @@ class ChunkUtilsTest { } @Test - void testUpdateChunksWithMultipleChunks() { + void testUpdateChunks_withMultipleChunks() { // Create a region spanning multiple chunks (16 blocks per chunk) Location loc1 = new Location(world, 0, 64, 0); Location loc2 = new Location(world, 32, 70, 32); + Cuboid region = new Cuboid(loc1, loc2); + when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getPlayers()).thenReturn(Collections.singletonList(player)); when(chunk.getWorld()).thenReturn(world); when(chunk.getX()).thenReturn(0); when(chunk.getZ()).thenReturn(0); - Cuboid region = new Cuboid(loc1, loc2); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); // Should retrieve multiple chunks (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2) @@ -87,20 +84,73 @@ class ChunkUtilsTest { } @Test - void testUpdateChunksWithNegativeCoordinates() { + void testUpdateChunks_withNegativeCoordinates() { // Test with negative coordinates Location loc1 = new Location(world, -16, 64, -16); Location loc2 = new Location(world, -1, 70, -1); + Cuboid region = new Cuboid(loc1, loc2); + when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); when(world.getPlayers()).thenReturn(Collections.singletonList(player)); when(chunk.getWorld()).thenReturn(world); when(chunk.getX()).thenReturn(-1); when(chunk.getZ()).thenReturn(-1); - Cuboid region = new Cuboid(loc1, loc2); assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); // Verify chunk was retrieved for negative coordinates verify(world, atLeastOnce()).getChunkAt(-1, -1); } + + @Test + void testUpdateChunks_withLargeRegion() { + // Test with a large region spanning many chunks + Location loc1 = new Location(world, 0, 0, 0); + Location loc2 = new Location(world, 100, 100, 100); + Cuboid region = new Cuboid(loc1, loc2); + + when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); + when(world.getPlayers()).thenReturn(Collections.emptyList()); + when(chunk.getWorld()).thenReturn(world); + + assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); + + // Should handle large regions without issues + // 100 blocks = ~7 chunks per axis, so ~49 chunks total + verify(world, atLeast(40)).getChunkAt(anyInt(), anyInt()); + } + + @Test + void testUpdateChunks_withNoPlayers() { + // Test behavior when no players are online + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 5, 70, 5); + Cuboid region = new Cuboid(loc1, loc2); + + when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); + when(world.getPlayers()).thenReturn(Collections.emptyList()); + when(chunk.getWorld()).thenReturn(world); + + // Should not throw exception even with no players + assertDoesNotThrow(() -> ChunkUtils.updateChunks(region)); + } + + @Test + void testChunkCoordinateCalculation() { + // Test that chunk coordinates are calculated correctly + // Block 0-15 = chunk 0, block 16-31 = chunk 1, etc. + + Location loc1 = new Location(world, 0, 64, 0); + Location loc2 = new Location(world, 15, 70, 15); + Cuboid singleChunk = new Cuboid(loc1, loc2); + + when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk); + when(world.getPlayers()).thenReturn(Collections.emptyList()); + when(chunk.getWorld()).thenReturn(world); + + ChunkUtils.updateChunks(singleChunk); + + // Should only access chunk (0,0) + verify(world, times(1)).getChunkAt(0, 0); + } } diff --git a/src/test/java/dev/loki/lomines/util/LocationParserErrorTest.java b/src/test/java/dev/loki/lomines/util/LocationParserErrorTest.java deleted file mode 100644 index 49ecf08..0000000 --- a/src/test/java/dev/loki/lomines/util/LocationParserErrorTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.loki.lomines.util; - -import dev.loki.lomines.data.config.parser.ConfigParseException; -import dev.loki.lomines.util.location.LocationParser; -import org.bukkit.Bukkit; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class LocationParserErrorTest { - - @Test - void testParseNullString() { - ConfigParseException exception = assertThrows( - ConfigParseException.class, - () -> LocationParser.parse(null) - ); - assertTrue(exception.getMessage().contains("cannot be null or empty")); - } - - @Test - void testParseEmptyString() { - ConfigParseException exception = assertThrows( - ConfigParseException.class, - () -> LocationParser.parse("") - ); - assertTrue(exception.getMessage().contains("cannot be null or empty")); - } - - @Test - void testParseInvalidFormat() { - ConfigParseException exception = assertThrows( - ConfigParseException.class, - () -> LocationParser.parse("world;100;64") - ); - assertTrue(exception.getMessage().contains("Invalid location format")); - assertTrue(exception.getMessage().contains("Expected format")); - } - - @Test - void testParseInvalidNumbers() { - ConfigParseException exception = assertThrows( - ConfigParseException.class, - () -> LocationParser.parse("world;abc;64;0;0;0") - ); - assertTrue(exception.getMessage().contains("Invalid location format")); - } - - @Test - void testParseNonExistentWorld() { - try (MockedStatic bukkit = Mockito.mockStatic(Bukkit.class)) { - bukkit.when(() -> Bukkit.getWorld("nonexistent")).thenReturn(null); - - ConfigParseException exception = assertThrows( - ConfigParseException.class, - () -> LocationParser.parse("nonexistent;0;64;0;0;0") - ); - assertTrue(exception.getMessage().contains("does not exist")); - assertTrue(exception.getMessage().contains("nonexistent")); - } - } -} diff --git a/src/test/java/dev/loki/lomines/util/LocationParserTest.java b/src/test/java/dev/loki/lomines/util/LocationParserTest.java index bc5a6fd..284a3a3 100644 --- a/src/test/java/dev/loki/lomines/util/LocationParserTest.java +++ b/src/test/java/dev/loki/lomines/util/LocationParserTest.java @@ -1,7 +1,6 @@ package dev.loki.lomines.util; import dev.loki.lomines.data.config.parser.ConfigParseException; -import dev.loki.lomines.util.location.LocationParser; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; @@ -9,10 +8,7 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -70,6 +66,57 @@ class LocationParserTest { } } + @Test + void testParseNullString() { + ConfigParseException exception = assertThrows( + ConfigParseException.class, + () -> LocationParser.parse(null) + ); + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + void testParseEmptyString() { + ConfigParseException exception = assertThrows( + ConfigParseException.class, + () -> LocationParser.parse("") + ); + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + void testParseInvalidFormat() { + ConfigParseException exception = assertThrows( + ConfigParseException.class, + () -> LocationParser.parse("world;100;64") + ); + assertTrue(exception.getMessage().contains("Invalid location format")); + assertTrue(exception.getMessage().contains("Expected format")); + } + + @Test + void testParseInvalidNumbers() { + ConfigParseException exception = assertThrows( + ConfigParseException.class, + () -> LocationParser.parse("world;abc;64;0;0;0") + ); + assertTrue(exception.getMessage().contains("Invalid location format")); + } + + @Test + void testParseNonExistentWorld() { + try (MockedStatic bukkit = Mockito.mockStatic(Bukkit.class)) { + bukkit.when(() -> Bukkit.getWorld("nonexistent")).thenReturn(null); + + ConfigParseException exception = assertThrows( + ConfigParseException.class, + () -> LocationParser.parse("nonexistent;0;64;0;0;0") + ); + assertTrue(exception.getMessage().contains("does not exist")); + assertTrue(exception.getMessage().contains("nonexistent")); + } + } + @Test void testFormatValidLocation() { World mockWorld = mock(World.class); diff --git a/src/test/java/dev/loki/lomines/util/TimeFormatterEdgeTest.java b/src/test/java/dev/loki/lomines/util/TimeFormatterEdgeTest.java deleted file mode 100644 index 6a465d1..0000000 --- a/src/test/java/dev/loki/lomines/util/TimeFormatterEdgeTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.loki.lomines.util; - -import dev.loki.lomines.util.format.TimeFormatter; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TimeFormatterEdgeTest { - - @Test - void testFormatNegativeTicks() { - assertEquals("00:00", TimeFormatter.format(-100, "mm:ss")); - assertEquals("00:00:00", TimeFormatter.format(-1000, "hh:mm:ss")); - assertEquals("0", TimeFormatter.format(-50, "s")); - } - - @Test - void testFormatNullFormat() { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> TimeFormatter.format(1200, null) - ); - assertTrue(exception.getMessage().contains("cannot be null or empty")); - } - - @Test - void testFormatEmptyFormat() { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> TimeFormatter.format(1200, "") - ); - assertTrue(exception.getMessage().contains("cannot be null or empty")); - } - - @Test - void testFormatWhitespaceFormat() { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> TimeFormatter.format(1200, " ") - ); - assertTrue(exception.getMessage().contains("cannot be null or empty")); - } - - @Test - void testFormatUnknownFormat() { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> TimeFormatter.format(1200, "invalid") - ); - assertTrue(exception.getMessage().contains("Unknown time format")); - assertTrue(exception.getMessage().contains("invalid")); - assertTrue(exception.getMessage().contains("Supported formats")); - } - - @Test - void testFormatLargeValues() { - assertEquals("833:20", TimeFormatter.format(1000000, "mm:ss")); - assertEquals("13:53:20", TimeFormatter.format(1000000, "hh:mm:ss")); - assertEquals("50000", TimeFormatter.format(1000000, "s")); - assertEquals("833", TimeFormatter.format(1000000, "m")); - assertEquals("13", TimeFormatter.format(1000000, "h")); - } -} diff --git a/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java b/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java index 13f20fb..f391c51 100644 --- a/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java +++ b/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java @@ -1,9 +1,9 @@ package dev.loki.lomines.util; -import dev.loki.lomines.util.format.TimeFormatter; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class TimeFormatterTest { @@ -103,6 +103,62 @@ class TimeFormatterTest { assertEquals("1", TimeFormatter.format(72000, "H")); } + @Test + void testFormatNegativeTicks() { + // Negative ticks should be treated as 0 + assertEquals("00:00", TimeFormatter.format(-100, "mm:ss")); + assertEquals("00:00:00", TimeFormatter.format(-1000, "hh:mm:ss")); + assertEquals("0", TimeFormatter.format(-50, "s")); + } + + @Test + void testFormatNullFormat() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> TimeFormatter.format(1200, null) + ); + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + void testFormatEmptyFormat() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> TimeFormatter.format(1200, "") + ); + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + void testFormatWhitespaceFormat() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> TimeFormatter.format(1200, " ") + ); + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + void testFormatUnknownFormat() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> TimeFormatter.format(1200, "invalid") + ); + assertTrue(exception.getMessage().contains("Unknown time format")); + assertTrue(exception.getMessage().contains("invalid")); + assertTrue(exception.getMessage().contains("Supported formats")); + } + + @Test + void testFormatLargeValues() { + // 1 million ticks = 50000 seconds = 833 minutes = 13 hours 53 minutes 20 seconds + assertEquals("833:20", TimeFormatter.format(1000000, "mm:ss")); + assertEquals("13:53:20", TimeFormatter.format(1000000, "hh:mm:ss")); + assertEquals("50000", TimeFormatter.format(1000000, "s")); + assertEquals("833", TimeFormatter.format(1000000, "m")); + assertEquals("13", TimeFormatter.format(1000000, "h")); + } + @Test void testFormatResetTimerExample() { // Typical reset timer: 6000 ticks (5 minutes) diff --git a/src/test/java/dev/loki/lomines/util/selection/SelectionBoundsTest.java b/src/test/java/dev/loki/lomines/util/selection/SelectionBoundsTest.java deleted file mode 100644 index ebbf45f..0000000 --- a/src/test/java/dev/loki/lomines/util/selection/SelectionBoundsTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package dev.loki.lomines.util.selection; - -import dev.loki.lomines.util.location.geo.Cuboid; -import org.bukkit.Location; -import org.bukkit.World; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class SelectionBoundsTest { - - private World world; - private Selection selection; - - @BeforeEach - void setUp() { - world = mock(World.class); - selection = new Selection(); - when(world.getName()).thenReturn("world"); - } - - @Test - void testToCuboidsWithNoPairs() { - List cuboids = selection.toCuboids(); - - assertTrue(cuboids.isEmpty()); - } - - @Test - void testToCuboidsWithOnePair() { - Location loc1 = new Location(world, 10, 64, 20); - Location loc2 = new Location(world, 20, 74, 30); - - selection.setPoint(0, loc1); - selection.setPoint(1, loc2); - - List cuboids = selection.toCuboids(); - - assertEquals(1, cuboids.size()); - assertTrue(cuboids.get(0).contains(new Location(world, 15, 69, 25))); - } - - @Test - void testToCuboidsWithMultiplePairs() { - Location loc1 = new Location(world, 10, 64, 20); - Location loc2 = new Location(world, 20, 74, 30); - Location loc3 = new Location(world, 30, 64, 40); - Location loc4 = new Location(world, 40, 74, 50); - - selection.setPoint(0, loc1); - selection.setPoint(1, loc2); - selection.setPoint(2, loc3); - selection.setPoint(3, loc4); - - List cuboids = selection.toCuboids(); - - assertEquals(2, cuboids.size()); - } - - @Test - void testToCuboidsSkipsIncompletePairs() { - Location loc1 = new Location(world, 10, 64, 20); - Location loc2 = new Location(world, 20, 74, 30); - Location loc3 = new Location(world, 30, 64, 40); - - selection.setPoint(0, loc1); - selection.setPoint(1, loc2); - selection.setPoint(2, loc3); - - List cuboids = selection.toCuboids(); - - assertEquals(1, cuboids.size()); - } - - @Test - void testPairInvariant() { - Location loc1 = new Location(world, 10, 64, 20); - Location loc2 = new Location(world, 20, 74, 30); - - selection.setPoint(0, loc1); - selection.setPoint(1, loc2); - assertTrue(selection.hasPair(0)); - - selection.setPoint(2, loc1); - selection.setPoint(3, loc2); - assertTrue(selection.hasPair(1)); - - selection.setPoint(4, loc1); - selection.setPoint(5, loc2); - assertTrue(selection.hasPair(2)); - - selection.setPoint(6, loc1); - selection.setPoint(7, loc2); - assertTrue(selection.hasPair(3)); - - selection.setPoint(8, loc1); - selection.setPoint(9, loc2); - assertTrue(selection.hasPair(4)); - } -} diff --git a/src/test/java/dev/loki/lomines/util/selection/SelectionManagerTest.java b/src/test/java/dev/loki/lomines/util/selection/SelectionManagerTest.java index aa1e57e..83fbe3b 100644 --- a/src/test/java/dev/loki/lomines/util/selection/SelectionManagerTest.java +++ b/src/test/java/dev/loki/lomines/util/selection/SelectionManagerTest.java @@ -6,13 +6,7 @@ import org.junit.jupiter.api.Test; import java.util.Optional; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class SelectionManagerTest { @@ -26,7 +20,7 @@ class SelectionManagerTest { } @Test - void getSelectionCreatesNewSelectionIfNotExists() { + void getSelection_createsNewSelectionIfNotExists() { Selection selection = manager.getSelection(playerId); assertNotNull(selection); @@ -34,7 +28,7 @@ class SelectionManagerTest { } @Test - void getSelectionReturnsSameInstanceOnMultipleCalls() { + void getSelection_returnsSameInstanceOnMultipleCalls() { Selection first = manager.getSelection(playerId); Selection second = manager.getSelection(playerId); @@ -42,21 +36,21 @@ class SelectionManagerTest { } @Test - void getSelectionThrowsExceptionForNullPlayerId() { + void getSelection_throwsExceptionForNullPlayerId() { assertThrows(IllegalArgumentException.class, () -> { manager.getSelection(null); }); } @Test - void findSelectionReturnsEmptyWhenNotExists() { + void findSelection_returnsEmptyWhenNotExists() { Optional result = manager.findSelection(playerId); assertTrue(result.isEmpty()); } @Test - void findSelectionReturnsSelectionWhenExists() { + void findSelection_returnsSelectionWhenExists() { Selection created = manager.getSelection(playerId); Optional found = manager.findSelection(playerId); @@ -65,33 +59,33 @@ class SelectionManagerTest { } @Test - void findSelectionThrowsExceptionForNullPlayerId() { + void findSelection_throwsExceptionForNullPlayerId() { assertThrows(IllegalArgumentException.class, () -> { manager.findSelection(null); }); } @Test - void hasSelectionReturnsFalseWhenNotExists() { + void hasSelection_returnsFalseWhenNotExists() { assertFalse(manager.hasSelection(playerId)); } @Test - void hasSelectionReturnsTrueWhenExists() { + void hasSelection_returnsTrueWhenExists() { manager.getSelection(playerId); assertTrue(manager.hasSelection(playerId)); } @Test - void hasSelectionThrowsExceptionForNullPlayerId() { + void hasSelection_throwsExceptionForNullPlayerId() { assertThrows(IllegalArgumentException.class, () -> { manager.hasSelection(null); }); } @Test - void clearSelectionRemovesSelection() { + void clearSelection_removesSelection() { manager.getSelection(playerId); assertTrue(manager.hasSelection(playerId)); @@ -101,14 +95,14 @@ class SelectionManagerTest { } @Test - void clearSelectionDoesNothingWhenSelectionNotExists() { + void clearSelection_doesNothingWhenSelectionNotExists() { assertDoesNotThrow(() -> { manager.clearSelection(playerId); }); } @Test - void clearSelectionThrowsExceptionForNullPlayerId() { + void clearSelection_throwsExceptionForNullPlayerId() { assertThrows(IllegalArgumentException.class, () -> { manager.clearSelection(null); }); diff --git a/src/test/java/dev/loki/lomines/util/selection/SelectionTest.java b/src/test/java/dev/loki/lomines/util/selection/SelectionTest.java index e5b12fd..bfe0a05 100644 --- a/src/test/java/dev/loki/lomines/util/selection/SelectionTest.java +++ b/src/test/java/dev/loki/lomines/util/selection/SelectionTest.java @@ -5,13 +5,10 @@ import org.bukkit.World; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -136,4 +133,90 @@ class SelectionTest { assertFalse(selection.getPoint(1).isPresent()); assertFalse(selection.getPoint(5).isPresent()); } + + @Test + void testToCuboidsWithNoPairs() { + List cuboids = selection.toCuboids(); + + assertTrue(cuboids.isEmpty()); + } + + @Test + void testToCuboidsWithOnePair() { + Location loc1 = new Location(world, 10, 64, 20); + Location loc2 = new Location(world, 20, 74, 30); + + selection.setPoint(0, loc1); + selection.setPoint(1, loc2); + + List cuboids = selection.toCuboids(); + + assertEquals(1, cuboids.size()); + assertTrue(cuboids.get(0).contains(new Location(world, 15, 69, 25))); + } + + @Test + void testToCuboidsWithMultiplePairs() { + Location loc1 = new Location(world, 10, 64, 20); + Location loc2 = new Location(world, 20, 74, 30); + Location loc3 = new Location(world, 30, 64, 40); + Location loc4 = new Location(world, 40, 74, 50); + + selection.setPoint(0, loc1); + selection.setPoint(1, loc2); + selection.setPoint(2, loc3); + selection.setPoint(3, loc4); + + List cuboids = selection.toCuboids(); + + assertEquals(2, cuboids.size()); + } + + @Test + void testToCuboidsSkipsIncompletePairs() { + Location loc1 = new Location(world, 10, 64, 20); + Location loc2 = new Location(world, 20, 74, 30); + Location loc3 = new Location(world, 30, 64, 40); + + selection.setPoint(0, loc1); + selection.setPoint(1, loc2); + selection.setPoint(2, loc3); + // Point 3 not set + + List cuboids = selection.toCuboids(); + + assertEquals(1, cuboids.size()); + } + + @Test + void testPairInvariant() { + // Test that pairs are organized as 2i and 2i+1 + Location loc1 = new Location(world, 10, 64, 20); + Location loc2 = new Location(world, 20, 74, 30); + + // Pair 0: indices 0 and 1 + selection.setPoint(0, loc1); + selection.setPoint(1, loc2); + assertTrue(selection.hasPair(0)); + + // Pair 1: indices 2 and 3 + selection.setPoint(2, loc1); + selection.setPoint(3, loc2); + assertTrue(selection.hasPair(1)); + + // Pair 2: indices 4 and 5 + selection.setPoint(4, loc1); + selection.setPoint(5, loc2); + assertTrue(selection.hasPair(2)); + + // Pair 3: indices 6 and 7 + selection.setPoint(6, loc1); + selection.setPoint(7, loc2); + assertTrue(selection.hasPair(3)); + + // Pair 4: indices 8 and 9 + selection.setPoint(8, loc1); + selection.setPoint(9, loc2); + assertTrue(selection.hasPair(4)); + } } diff --git a/todo.md b/todo.md index 27e527f..3bc1880 100644 --- a/todo.md +++ b/todo.md @@ -1,601 +1,477 @@ -# LoMines — План реструктуризации и улучшения качества +# LoMines - TODO List (Обновлено: 2026-05-31) -> **Обновлено:** 2026-08-12 -> **Цели:** Разбивка на подпапки (≤3 файла), лимит 150 строк/файл, Checkstyle, тестовое покрытие +> **Статус проекта:** ✅ Система конфигурации полностью переписана +> **Базовая библиотека:** LoAPI (jar из `libs/lolib*.jar`, сейчас 3.0.0) +> **Версия Minecraft:** Paper 1.21.4 +> **Java:** 21 +> **Пакет:** dev.loki.lomines (изменен с com.loki) --- -## 📊 Текущее состояние +## ✅ Что сделано сегодня (2026-05-31) -### Файлы >150 строк (нужен split) — 25 main + 11 test +### 1. Рефакторинг пакетов ✅ +- `com.loki` → `dev.loki` +- Все Java файлы обновлены +- `plugin.yml` обновлен +- Исправлены импорты в тестах + +### 2. Полная перепись системы конфигурации ✅ +- Section-based конфигурация с type-safe BlockKey +- Human-readable durations (5m, 30s, 2h) +- YAML inheritance через defaults.yml +- MiniMessage поддержка для форматирования + +### 3. Обновление core классов ✅ +- `Mine.java` — использует RegionConfig, BlockConfig, UIConfig +- `Mines.java` — использует новый ConfigLoader +- `MineFileManager.java` — создание конфигов с новой системой +- `MineRepository.java` — работа с новым MineConfig +- `MaskScanService.java` — использует BlockKey для маркеров +- `BukkitBlockSetter.java` — Map + +### 4. Обновление обработчиков ✅ +- `ActionBarHandler.java` — UIConfig с MiniMessage +- `MineBlockHandler.java` — ResetConfig +- `MineRewardHandler.java` — RewardConfig +- `MineResetHandler.java` — новые секции конфигурации + +### 5. Автодополнение команд ✅ +- `LoMinesTabCompleter.java` — tab completion для всех команд +- Подсказки для имён шахт, игроков, субкоманд +- Permission-based фильтрация + +### 6. GUI редакторы ✅ (1/6 основных) +- `MineEditGui.java` — главный редактор шахты + - Просмотр всех секций конфигурации + - Навигация к под-редакторам (заглушки) + - Сохранение и удаление с подтверждением +- `ConfirmDeleteGui.java` — подтверждение удаления +- `MineEditGuiListener.java` — обработка кликов в GUI +- Команда `/lm edit ` для открытия редактора +- Разрешение `lomines.admin.edit` + +### 7. Команды для установки точек телепортации и спавна ✅ +- `/lm setteleport ` — установить точку телепорта при сбросе + - Берёт текущую позицию игрока (X, Y, Z, yaw, pitch, world) + - Право: `lomines.admin.setteleport` +- `/lm setspawn ` — установить точку спавна для застрявших + - Отдельная точка для игроков, застрявших в блоках + - Право: `lomines.admin.setspawn` +- `/lm clearspawn ` — удалить точку спавна + - Используется fallback на `teleport` точку + - Право: `lomines.admin.setspawn` +- Auto-completion для всех команд +- `MineEditGui` показывает статус спавна и подсказки команд + +### 8. Player-spawn для застрявших игроков ✅ +- `PlayerSpawnConfig.java` — настройка точки спавна для застрявших игроков +- `PlayerSpawnConfigLoader.java` — загрузка из YAML +- Обновлен `MineConfig`: + - `playerSpawn` — отдельная точка для телепорта застрявших + - `getSpawnForStuckPlayer()` — возвращает player-spawn или fallback на teleport +- Обновлен `MineResetHandler.teleportPlayers()`: + - Использует `player-spawn` если настроен + - Иначе использует `teleport` (backward compatibility) +- `findSafeTeleportLocation()` с ограничением вверх: + - Максимум 3 блока вверх (не телепортирует слишком высоко) + - Приоритет: соседние блоки (тот же Y) → вверх (max 3) → вниз → диагональ + +### 9. Ghost blocks фикс ✅ +- `BlockUpdateUtil.java` — утилита для отправки пакетов обновления блоков + - `sendBlockUpdate()` — обновить один блок для всех видящих игроков + - `sendRegionUpdate()` — batch-обновление для кубоидного региона + - `sendLocationsUpdate()` — эффективное обновление для списка позиций + - `refreshChunks()` — полный resync чанков при необходимости + - `findSafeTeleportLocation()` — найти безопасную позицию без задыхания +- `BukkitBlockSetter` — отправляет пакеты после установки блоков +- `MineResetHandler.teleportPlayers()` — телепортирует в безопасную позицию + +**Проблема:** Ghost blocks (невидимые блоки) возникают при быстрой установке блоков, когда сервер не отправляет пакеты клиенту. +**Решение:** Принудительная отправка `player.sendBlockChange()` после установки блоков. + +### 10. PlaceholderAPI интеграция ✅ +- `LoMinesPlaceholderExpansion.java` — расширение PlaceholderAPI + - `%lomines_mine__name%` — название шахты + - `%lomines_mine__blocks%` — текущее количество блоков + - `%lomines_mine__total%` — общий объём + - `%lomines_mine__percent%` — процент заполнения (1 знак) + - `%lomines_mine__percentint%` — процент (целое) + - `%lomines_mine__world%` — мир шахты + - `%lomines_mine__remaining%` — оставшиеся блоки + - `%lomines_mine__resettime%` — время с последнего сброса (mm:ss) + - `%lomines_mine__resetseconds%` — секунды с сброса + - `%lomines_player_blocksmined%` — всего добыто блоков + - `%lomines_player_minesreset%` — сброшено шахт + - `%lomines_player_playtime%` — время игры (форматированное) + - `%lomines_player_rank%` — позиция в топе + - `%lomines_count%` — количество шахт +- `IntegrationManager` — регистрация/отключение расширения +- Oraxen и ItemsAdder интеграции отключены (не реализованы) + +### 12. Holograms (голограммы над шахтами) 📋 +- **Показывают:** + - Текущий % заполнения шахты + - Таймер до следующего сброса + - Название шахты +- **Интеграции:** + - HolographicDisplays + - DecentHolograms +- **Конфигурация:** `hologram.enabled`, `hologram.format`, `hologram.height` +- **Команды:** `/lm hologram [on|off]` + +### 13. Sub-GUI редакторы (в разработке) 📋 +Текущий статус: только главное меню `MineEditGui` (заглушки для под-редакторов) + +Нужно реализовать: + +#### BlocksGui — редактор блоков и шансов +- Список всех блоков с весами (MATERIAL + шанс) +- Добавление нового блока (инвентарь с доступными материалами) +- Изменение веса блока (клик ЛКМ/ПКМ для +/-) +- Удаление блока (Shift+ПКМ) +- Просмотр FillMode (CUBOID/MASK) +- Интеграция с Oraxen/ItemsAdder (если включено) + +#### ResetGui — настройка сброса шахты +- Интервал сброса (поле ввода с форматом 5m, 30s, 2h) +- Процентный триггер (вкл/выкл + значение) +- Сообщение при сбросе (поле ввода с поддержкой MiniMessage) +- Список команд при сбросе (добавление/удаление) + +#### RewardsGui — настройка наград +- Список записей наград (chance + типы блоков) +- Редактирование шанса награды +- Добавление/удаление типов блоков для награды +- Настройка предметов (items) — инвентарь для создания ItemStack +- Настройка команд (commands) — список с добавлением/удалением +- Флаг prevent-vanilla-drops + +### 11. WorldGuard интеграция ✅ +- `WorldGuardConfig.java` — настройки авто-регионов + - Шаблоны имён: `{mine_name}_{random_4}`, `{random_6}`, и т.д. + - Настройка владельцев и членов (name или uuid:xxx) + - Настройка флагов (passthrough, build, pvp, tnt, и т.д.) + - Включение/выключение авто-создания +- `WorldGuardRegionService.java` — создание/обновление/удаление регионов +- `WorldGuardFlagParser.java` — парсинг флагов из строк +- `WorldGuardConfigLoader.java` — загрузка из YAML +- Автоматическое создание региона при создании шахты +- Автоматическое удаление при удалении шахты +- Обновление при перезагрузке шахты + +### 11. Рефакторинг больших файлов ✅ +- `ConfigLoader.java`: 427 → 151 строк (split на 6 лоадеров) +- `WorldGuardRegionService.java`: split флаг-парсер +- Новые лоадеры: BlockConfigLoader, RegionConfigLoader, ResetConfigLoader, RewardConfigLoader, TeleportConfigLoader, UIConfigLoader +- `MineEditGui.java` — главный редактор шахты + - Просмотр всех секций конфигурации + - Навигация к под-редакторам (заглушки) + - Сохранение и удаление с подтверждением +- `ConfirmDeleteGui.java` — подтверждение удаления +- `MineEditGuiListener.java` — обработка кликов в GUI +- Команда `/lm edit ` для открытия редактора +- Разрешение `lomines.admin.edit` + +#### Новая архитектура (Section-based): ``` -MAIN (нуждаются в сплите): - 209 handler/MineResetHandler.java - 207 util/format/ColorUtils.java - 207 command/common/LoMinesTabCompleter.java - 203 integration/hologram/HologramManager.java - 201 core/service/MineFileManager.java - 190 gui/mine/main/MineEditItems.java - 186 command/admin/StatsCommands.java - 181 util/location/Cuboid.java - 181 data/stats/StatsManager.java - 180 gui/mine/edit/blocks/BlockMaterialSelectionGui.java - 174 util/format/ChunkUtils.java - 173 listener/MineEditGuiListener.java - 170 core/mine/Mine.java - 165 command/admin/RegionCommands.java - 164 data/config/ConfigLoader.java - 163 integration/worldguard/WorldGuardRegionService.java - 161 data/config/MineConfig.java - 161 command/admin/AdminCommands.java - 160 util/block/BlockUpdateUtil.java - 156 data/config/reset/ResetConfig.java - 155 data/reward/RewardParser.java - 155 command/admin/TeleportCommands.java - 154 integration/worldguard/WorldGuardConfig.java - 153 core/mine/Mines.java - 151 gui/mine/edit/blocks/BlocksGui.java - -TEST (нуждаются в сплите): - 264 core/MinesTest.java - 262 data/StatsManagerTest.java - 223 util/selection/SelectionTest.java - 210 data/LeaderboardTest.java - 191 data/config/MineConfigTest.java - 181 util/LocationParserTest.java - 178 util/TimeFormatterTest.java - 169 data/config/reset/ResetConfigTest.java - 159 data/LeaderboardCacheIntegrationTest.java - 158 util/ChunkUtilsTest.java - 152 command/AdminCommandsTest.java +data/config/ +├── block/ +│ ├── BlockKey.java # Sealed interface: Vanilla, Oraxen, ItemsAdder +│ ├── BlockConfig.java # Type-safe block weights +│ └── FillMode.java # CUBOID / MASK +├── region/ +│ └── RegionConfig.java # Cuboid regions +├── reset/ +│ └── ResetConfig.java # Duration, triggers, commands +├── reward/ +│ └── RewardConfig.java # ItemReward with MiniMessage +├── teleport/ +│ └── TeleportConfig.java # Teleport on reset +├── ui/ +│ └── UIConfig.java # Action bar, timer format +├── MineConfig.java # Composed record +└── ConfigLoader.java # Clean loader with YAML inheritance ``` -### Папки с >3 файлами (нужны подпапки) -``` - 8 command/admin/ → manage/, region/, stats/, teleport/, misc/ - 6 listener/ → block/, gui/, player/ - 5 handler/ → reset/, block/, reward/, ui/ - 5 gui/mine/holder/ → edit/, main/ (перегруппировать) - 5 data/reward/ → model/, parser/ - 4 util/location/ → оставить (package-info не считается) - 4 util/ (root) → split ErrorHandler, MessageFormatter, ValidationUtils - 4 integration/worldguard/ → config/, service/ - 4 data/stats/ → model/, service/, query/ - 4 core/mine/ → model/, registry/, loader/, tick/ -``` +#### Удалена старая система: +- ❌ `ConfigParser.java` (464 строк) +- ❌ `ConfigValidator.java` (валидация размазана) +- ❌ `ConfigSerializer.java` (сериализация размазана) +- ❌ `MineConfigBuilder.java` (builder внутри record) +- ❌ `parser/ConfigParseException.java` +- ❌ Старый `MineConfig.java` (17 полей) + +#### Новые возможности: +- ✅ **Type-safe BlockKey** — sealed interface с Vanilla, Oraxen, ItemsAdder +- ✅ **Human-readable durations** — "5m", "30s", "2h", "1d" +- ✅ **YAML inheritance** — `defaults.yml` + перезапись в mine.yml +- ✅ **MiniMessage** — форматирование action bar и предметов +- ✅ **Immutable records** — все конфиги неизменяемы +- ✅ **Валидация на уровне конструктора** — fail-fast --- -## 🎯 Фазы работ +## 📋 Текущий прогресс -### Phase 1: Настройка Checkstyle -- [ ] Создать `config/checkstyle/checkstyle.xml` с правилами: - - Max line length: 150 - - Max file length: 150 строк - - Max method length: 30 строк - - No unused imports - - No wildcard imports - - Indentation: 4 spaces - - Javadoc style enforced -- [ ] Добавить плагин checkstyle в `build.gradle.kts` -- [ ] Проверить, что билд не падает (пока warnings mode) - -### Phase 2: Split файлов >150 строк (main) - -#### 2.1 handler/MineResetHandler.java (209→~110+~90) -- [ ] Выделить `PlayerTeleportHandler.java` — логика телепортации stuck players -- [ ] В `MineResetHandler` остаётся: reset, resetAllRegions, onResetComplete, executeResetCommands, broadcastReset - -#### 2.2 util/format/ColorUtils.java (207→~70+~90+~40) -- [ ] Выделить `LegacyColorConverter.java` — legacy &a → MiniMessage -- [ ] Выделить `HexColorConverter.java` — &#RRGGBB → <#RRGGBB>, &x→MiniMessage -- [ ] В `ColorUtils` остаётся: format(), stripColors(), toLegacy() как фасад - -#### 2.3 command/common/LoMinesTabCompleter.java (207→~60+~70+~70) -- [ ] Выделить `SubcommandCompleter.java` — completeSecondArg, completeThirdArg -- [ ] Выделить `PermissionPredicate.java` — hasPermission для всех команд -- [ ] В `LoMinesTabCompleter` остаётся: onTabComplete, isLoMinesCommand, completeSubcommands, filterStartsWith, хелперы - -#### 2.4 integration/hologram/HologramManager.java (203→~100+~100) -- [ ] Выделить `HologramRenderer.java` — рендеринг строк голограммы -- [ ] В `HologramManager` остаётся: create/update/delete/toggle, управление жизненным циклом - -#### 2.5 core/service/MineFileManager.java (201→~100+~100) -- [ ] Выделить `MineConfigWriter.java` — создание/сохранение mine.yml -- [ ] В `MineFileManager` остаётся: загрузка, поиск, листинг файлов - -#### 2.6 gui/mine/main/MineEditItems.java (190→~95+~95) -- [ ] Выделить `MineEditItemActions.java` — обработчики кликов для каждого слота -- [ ] В `MineEditItems` остаётся: создание ItemStack, статические методы - -#### 2.7 command/admin/StatsCommands.java (186→~90+~90) -- [ ] Выделить `LeaderboardRenderer.java` — форматирование и отправка топ-списка -- [ ] В `StatsCommands` остаётся: парсинг аргументов, делегирование - -#### 2.8 util/location/Cuboid.java (181→~90+~90) -- [ ] Выделить `CuboidSerializer.java` — сериализация/десериализация Cuboid -- [ ] В `Cuboid` остаётся: contains, volume, пересечения, геометрия - -#### 2.9 data/stats/StatsManager.java (181→~90+~90) -- [ ] Выделить `StatsPersistence.java` — load/save данных из файлов -- [ ] В `StatsManager` остаётся: getOrCreate, increment, кэш в ConcurrentHashMap - -#### 2.10 gui/mine/edit/blocks/BlockMaterialSelectionGui.java (180→~90+~90) -- [ ] Выделить `BlockMaterialSelector.java` — логика выбора материала -- [ ] В `BlockMaterialSelectionGui` остаётся: рендеринг GUI - -#### 2.11 util/format/ChunkUtils.java (174→~85+~85) -- [ ] Выделить `ChunkRefresher.java` — рефреш чанков -- [ ] В `ChunkUtils` остаётся: определение Paper, базовые операции - -#### 2.12 listener/MineEditGuiListener.java (173→~85+~85) -- [ ] Выделить `GuiActionHandler.java` — обработка действий в GUI -- [ ] В `MineEditGuiListener` остаётся: маршрутизация событий - -#### 2.13 core/mine/Mine.java (170→~85+~85) -- [ ] Выделить `MineState.java` — управление состоянием (блоки, тики, paused) -- [ ] В `Mine` остаётся: основные методы, делегирование - -#### 2.14 command/admin/RegionCommands.java (165→~80+~80) -- [ ] Выделить `RegionActionHandler.java` — add/remove region -- [ ] В `RegionCommands` остаётся: список, инфо, парсинг - -#### 2.15 data/config/ConfigLoader.java (164→~80+~80) -- [ ] Выделить `DefaultsMerger.java` — YAML inheritance из defaults.yml -- [ ] В `ConfigLoader` остаётся: загрузка mine.yml, вызов лоадеров - -#### 2.16 integration/worldguard/WorldGuardRegionService.java (163→~80+~80) -- [ ] Выделить `RegionTemplateRenderer.java` — рендеринг шаблонов имён -- [ ] В `WorldGuardRegionService` остаётся: create/update/delete - -#### 2.17 data/config/MineConfig.java (161→~80+~80) -- [ ] Выделить `MineConfigDefaults.java` — статические фабрики и Builder по умолчанию -- [ ] В `MineConfig` остаётся: record + базовый builder - -#### 2.18 command/admin/AdminCommands.java (161→~80+~80) -- [ ] Выделить `MineActionHandler.java` — create, delete, reset, reload -- [ ] В `AdminCommands` остаётся: list, edit, парсинг - -#### 2.19 util/block/BlockUpdateUtil.java (160→~80+~80) -- [ ] Выделить `SafeTeleportFinder.java` — findSafeTeleportLocation -- [ ] В `BlockUpdateUtil` остаётся: sendBlockUpdate, sendRegionUpdate, refreshChunks - -#### 2.20 data/config/reset/ResetConfig.java (156→~80+~75) -- [ ] Выделить `ResetConfigValidator.java` — валидация Duration, bounds -- [ ] В `ResetConfig` остаётся: record + builder - -#### 2.21 data/reward/RewardParser.java (155→~75+~80) -- [ ] Выделить `RewardEntryParser.java` — парсинг одной записи награды -- [ ] В `RewardParser` остаётся: парсинг всего списка - -#### 2.22 command/admin/TeleportCommands.java (155→~75+~80) -- [ ] Выделить `TeleportActionHandler.java` — setteleport, setspawn, clearspawn -- [ ] В `TeleportCommands` остаётся: парсинг команд - -#### 2.23 integration/worldguard/WorldGuardConfig.java (154→~75+~75) -- [ ] Выделить `RegionTemplateConfig.java` — шаблоны имён -- [ ] В `WorldGuardConfig` остаётся: record, flags, members - -#### 2.24 core/mine/Mines.java (153→~75+~75) -- [ ] Выделить `MineFinder.java` — find, findByRegion, getAll -- [ ] В `Mines` остаётся: create, delete, reloadMine, updateMineConfig - -#### 2.25 gui/mine/edit/blocks/BlocksGui.java (151→~75+~75) -- [ ] Выделить `BlockWeightEditor.java` — изменение весов блоков -- [ ] В `BlocksGui` остаётся: список блоков, рендеринг - -### Phase 3: Разбивка папок (max 3 файла) - -#### 3.1 command/admin (8 файлов) → 5 подпапок -- [ ] `command/admin/manage/` — AdminCommands.java, InfoCommand.java, CopyCommand.java -- [ ] `command/admin/region/` — RegionCommands.java -- [ ] `command/admin/stats/` — StatsCommands.java -- [ ] `command/admin/teleport/` — TeleportCommands.java -- [ ] `command/admin/misc/` — MaskCommands.java, HologramCommands.java - -#### 3.2 listener (6 файлов) → 3 подпапки -- [ ] `listener/block/` — BlockBreakListener.java -- [ ] `listener/gui/` — MineEditGuiListener.java, GroupGuiListener.java -- [ ] `listener/player/` — PlayerJoinListener.java, PlayerInteractListener.java - -#### 3.3 handler (5 файлов) → 4 подпапки -- [ ] `handler/reset/` — MineResetHandler.java (+ PlayerTeleportHandler из Phase 2) -- [ ] `handler/block/` — MineBlockHandler.java -- [ ] `handler/reward/` — MineRewardHandler.java -- [ ] `handler/ui/` — ActionBarHandler.java - -#### 3.4 gui/mine/holder (5 файлов) → 2 подпапки -- [ ] `gui/mine/holder/main/` — MineEditGuiHolder.java -- [ ] `gui/mine/holder/edit/` — BlocksGuiHolder.java, ResetGuiHolder.java, RewardsGuiHolder.java, BlockMaterialSelectionGuiHolder.java - -#### 3.5 data/reward (5 файлов) → 2 подпапки -- [ ] `data/reward/model/` — Reward.java -- [ ] `data/reward/parser/` — RewardParser.java, RewardItemParser.java, RewardCommandParser.java, RewardMaterialParser.java - -#### 3.6 util/location (4 файла) — оставить (с учётом package-info) -- [ ] Убедиться что лимит соблюдён: BlockKeys.java, Cuboid.java, LocationParser.java, SafeTeleportUtil.java -- [ ] Вынести package-info.java если мешает - -#### 3.7 util (root, 4 файла) — оставить -- [ ] ErrorHandler.java, MessageFormatter.java, ValidationUtils.java, package-info.java — ровно 3+package-info - -#### 3.8 integration/worldguard (4 файла) → 2 подпапки -- [ ] `integration/worldguard/config/` — WorldGuardConfig.java -- [ ] `integration/worldguard/service/` — WorldGuardRegionService.java, WorldGuardFlagParser.java, WorldGuardMemberHandler.java - -#### 3.9 data/stats (4 файла) → 2 подпапки -- [ ] `data/stats/model/` — PlayerStats.java, LeaderboardEntry.java -- [ ] `data/stats/service/` — StatsManager.java, Leaderboard.java - -#### 3.10 core/mine (4 файла) → 3 подпапки -- [ ] `core/mine/model/` — Mine.java -- [ ] `core/mine/registry/` — Mines.java -- [ ] `core/mine/service/` — MineLoader.java, MineTicker.java - -### Phase 4: Split тестов >150 строк - -#### 4.1 core/MinesTest.java (264→~130+~130) -- [ ] Сплитнуть на `MinesRegistryTest.java`, `MinesLifecycleTest.java` - -#### 4.2 data/StatsManagerTest.java (262→~130+~130) -- [ ] Сплитнуть на `StatsManagerStorageTest.java`, `StatsManagerConcurrencyTest.java` - -#### 4.3 util/selection/SelectionTest.java (223→~110+~110) -- [ ] Сплитнуть на `SelectionStateTest.java`, `SelectionBoundsTest.java` - -#### 4.4 data/LeaderboardTest.java (210→~105+~105) -- [ ] Сплитнуть на `LeaderboardSortingTest.java`, `LeaderboardQueryTest.java` - -#### 4.5 data/config/MineConfigTest.java (191→~95+~95) -- [ ] Сплитнуть на `MineConfigBuilderTest.java`, `MineConfigValidationTest.java` - -#### 4.6 util/LocationParserTest.java (181→~90+~90) -- [ ] Сплитнуть на `LocationParserValidTest.java`, `LocationParserErrorTest.java` - -#### 4.7 util/TimeFormatterTest.java (178→~90+~90) -- [ ] Сплитнуть по форматам: `TimeFormatterBasicTest.java`, `TimeFormatterEdgeTest.java` - -#### 4.8 data/config/reset/ResetConfigTest.java (169→~85+~85) -- [ ] Сплитнуть на `ResetConfigIntervalTest.java`, `ResetConfigValidationTest.java` - -#### 4.9 data/LeaderboardCacheIntegrationTest.java (159→~80+~80) -- [ ] Сплитнуть на `LeaderboardCacheTest.java`, `LeaderboardIntegrationTest.java` - -#### 4.10 util/ChunkUtilsTest.java (158→~80+~80) -- [ ] Сплитнуть на `ChunkUtilsPaperTest.java`, `ChunkUtilsRefreshTest.java` - -#### 4.11 command/AdminCommandsTest.java (152→~75+~75) -- [ ] Сплитнуть на `AdminCreateDeleteTest.java`, `AdminResetReloadTest.java` - -### Phase 5: Обновление импортов -- [ ] Обновить все import statements в затронутых файлах -- [ ] Обновить RegistrationManager.java — новые пути классов -- [ ] Обновить ComponentInitializer.java — новые пути -- [ ] Проверить сборку: `./gradlew build` - -### Phase 6: Проверка качества -- [ ] Запустить `./gradlew check` — убедиться что checkstyle не падает -- [ ] Запустить `./gradlew test` — все тесты проходят -- [ ] Проверить lint — отсутствие ошибок -- [ ] Финальный обзор структуры папок +- [x] **Phase 1:** Структура проекта и конфигурация ✅ +- [x] **Phase 2:** Core классы и handlers ✅ +- [x] **Phase 3:** BlockSetter — тип по `Map` и prefix ✅ +- [x] **Phase 4:** Команды — разбито на 4 класса ✅ +- [x] **Phase 5:** Рефакторинг больших файлов ✅ +- [x] **Phase 6:** Организация папок (все ≤6 файлов) ✅ +- [x] **Phase 7:** Утилиты и хелперы ✅ +- [x] **Phase 8:** Рефакторинг пакетов com.loki → dev.loki ✅ +- [x] **Phase 9:** Полная перепись системы конфигурации ✅ +- [x] **Phase 10:** Обновление core классов для новой системы ✅ +- [x] **Phase 11:** Обновление handlers для новой конфигурации ✅ +- [x] **Phase 12:** Автодополнение команд (tab completer) ✅ +- [x] **Phase 13:** GUI редакторы - главное меню ✅ (под-редакторы 0/3) + - [ ] BlocksGui — редактирование блоков и шансов + - [ ] ResetGui — настройка сброса + - [ ] RewardsGui — настройка наград +- [x] **Phase 14:** WorldGuard авто-создание регионов ✅ +- [ ] **Phase 15:** Holograms — голограммы над шахтами (процент, таймер) +- [ ] **Phase 16:** Интеграции PlaceholderAPI (✅), Oraxen, ItemsAdder --- -## 📁 Целевая структура проекта +## 🎯 Следующие шаги + +### ✅ Приоритет 1: Интеграция новой конфигурации (ВЫПОЛНЕНО) +- [x] Обновить `Mine.java` — использовать новые `RegionConfig`, `BlockConfig` +- [x] Обновить `Mines.java` — использовать новый `ConfigLoader` +- [x] Обновить `MineFileManager.java` — миграция на новый loader +- [x] Обновить `MineRepository.java` — работа с новым `MineConfig` +- [x] Обновить `BukkitBlockSetter.java` — использовать `BlockKey` +- [x] Обновить все handlers — ActionBarHandler, MineBlockHandler, MineResetHandler, MineRewardHandler +- [x] Обновить команды — MaskCommands +- [x] Добавить автодополнение — LoMinesTabCompleter + +### Приоритет 2: Интеграции с плагинами +1. [ ] Включить `OraxenBlockSetter.java` — использовать `BlockKey.Oraxen` +2. [ ] Включить `ItemsAdderBlockSetter.java` — использовать `BlockKey.ItemsAdder` +3. [ ] PlaceholderAPI интеграция + +### Приоритет 3: Sub-GUI редакторы ✅ (3/3) +- [x] `MineEditGui` — главное меню редактора (просмотр + навигация) +- [x] **BlocksGui** — редактор блоков и весов + - Список блоков с весами (MATERIAL + шанс) + - Добавление нового блока (меню выбора материала) + - Изменение веса блока (ЛКМ/ПКМ для +/-) + - Удаление блока (Shift+ПКМ) + - Поддержка Oraxen/ItemsAdder +- [x] **ResetGui** — настройка сброса шахты + - Интервал (формат: 5m, 30s, 2h) + - Процентный триггер (on/off + значение) + - Сообщение при сбросе (MiniMessage) + - Список команд +- [x] **RewardsGui** — настройка наград + - Список записей наград (chance + типы блоков) + - Редактор шанса и типов блоков + - Редактор предметов (drag & drop) + - Редактор команд + - Флаг prevent-vanilla-drops + +### Приоритет 4: Holograms (голограммы над шахтами) ✅ +- [x] `HologramManager.java` — управление всеми голограммами +- [x] `HologramProvider.java` — интерфейс провайдеров +- [x] Поддержка провайдеров: + - HolographicDisplays (основной) + - DecentHolograms (альтернатива) +- [x] Формат строк: + - Название шахты + - % заполнения (прогресс-бар) + - Таймер до сброса (mm:ss) +- [x] Конфигурация: `ui.hologram.enabled`, `ui.hologram.format`, `ui.hologram.height` +- [x] Команда `/lm hologram [on|off]` + +--- + +## 📁 Структура проекта ``` dev.loki.lomines/ -├── LoMinesPlugin.java # ≤120 -├── ComponentInitializer.java # ≤150 -├── RegistrationManager.java # ≤150 -│ +├── LoMinesPlugin.java +├── ComponentInitializer.java +├── RegistrationManager.java ├── command/ -│ ├── common/ -│ │ ├── LoMinesTabCompleter.java # ≤150 (split from 207) -│ │ ├── SubcommandCompleter.java # NEW -│ │ └── PermissionPredicate.java # NEW -│ ├── player/ -│ │ └── PlayerCommands.java -│ └── admin/ -│ ├── manage/ -│ │ ├── AdminCommands.java # ≤150 (split from 161) -│ │ ├── InfoCommand.java -│ │ └── CopyCommand.java -│ ├── region/ -│ │ └── RegionCommands.java # ≤150 (split from 165) -│ ├── stats/ -│ │ ├── StatsCommands.java # ≤150 (split from 186) -│ │ └── LeaderboardRenderer.java # NEW -│ ├── teleport/ -│ │ ├── TeleportCommands.java # ≤150 (split from 155) -│ │ └── TeleportActionHandler.java # NEW -│ └── misc/ -│ ├── MaskCommands.java -│ └── HologramCommands.java -│ +│ ├── AdminCommands.java +│ ├── PlayerCommands.java +│ ├── StatsCommands.java +│ └── MaskCommands.java ├── core/ -│ ├── mine/ -│ │ ├── model/ -│ │ │ ├── Mine.java # ≤150 (split from 170) -│ │ │ └── MineState.java # NEW -│ │ ├── registry/ -│ │ │ ├── Mines.java # ≤150 (split from 153) -│ │ │ └── MineFinder.java # NEW -│ │ └── service/ -│ │ ├── MineLoader.java -│ │ └── MineTicker.java +│ ├── Mine.java +│ ├── Mines.java +│ ├── MineTicker.java │ └── service/ -│ ├── MineFileManager.java # ≤150 (split from 201) -│ ├── MineConfigWriter.java # NEW +│ ├── MineFileManager.java │ ├── MineRepository.java │ └── MaskScanService.java -│ ├── data/ -│ ├── config/ -│ │ ├── MineConfig.java # ≤150 (split from 161) -│ │ ├── MineConfigDefaults.java # NEW -│ │ ├── ConfigLoader.java # ≤150 (split from 164) -│ │ ├── DefaultsMerger.java # NEW -│ │ ├── block/ -│ │ │ ├── BlockKey.java -│ │ │ ├── BlockConfig.java -│ │ │ └── FillMode.java -│ │ ├── region/ -│ │ │ └── RegionConfig.java -│ │ ├── reset/ -│ │ │ ├── ResetConfig.java # ≤150 (split from 156) -│ │ │ └── ResetConfigValidator.java # NEW -│ │ ├── reward/ -│ │ │ └── RewardConfig.java -│ │ ├── teleport/ -│ │ │ └── TeleportConfig.java -│ │ ├── spawn/ -│ │ │ └── PlayerSpawnConfig.java -│ │ ├── ui/ -│ │ │ ├── UIConfig.java -│ │ │ └── HologramConfig.java -│ │ ├── parser/ -│ │ │ └── ConfigParseException.java -│ │ └── loader/ -│ │ ├── block/BlockConfigLoader.java -│ │ ├── entity/ -│ │ │ ├── TeleportConfigLoader.java -│ │ │ └── PlayerSpawnConfigLoader.java -│ │ ├── region/ -│ │ │ ├── RegionConfigLoader.java -│ │ │ └── WorldGuardConfigLoader.java -│ │ ├── reward/RewardConfigLoader.java -│ │ └── system/ -│ │ ├── ResetConfigLoader.java -│ │ └── UIConfigLoader.java -│ ├── stats/ -│ │ ├── model/ -│ │ │ ├── PlayerStats.java -│ │ │ └── LeaderboardEntry.java -│ │ └── service/ -│ │ ├── StatsManager.java # ≤150 (split from 181) -│ │ ├── StatsPersistence.java # NEW -│ │ └── Leaderboard.java -│ └── reward/ -│ ├── model/ -│ │ └── Reward.java -│ └── parser/ -│ ├── RewardParser.java # ≤150 (split from 155) -│ ├── RewardEntryParser.java # NEW -│ ├── RewardItemParser.java -│ ├── RewardCommandParser.java -│ └── RewardMaterialParser.java -│ +│ └── config/ ← ✅ ПЕРЕПИСАНО +│ ├── block/ +│ │ ├── BlockKey.java +│ │ ├── BlockConfig.java +│ │ └── FillMode.java +│ ├── region/ +│ │ └── RegionConfig.java +│ ├── reset/ +│ │ └── ResetConfig.java +│ ├── reward/ +│ │ └── RewardConfig.java +│ ├── spawn/ ← ✅ NEW: Player spawn config +│ │ └── PlayerSpawnConfig.java +│ ├── teleport/ +│ │ └── TeleportConfig.java +│ ├── ui/ +│ │ └── UIConfig.java +│ ├── loader/ ← ✅ SPLIT FROM ConfigLoader +│ │ ├── BlockConfigLoader.java +│ │ ├── PlayerSpawnConfigLoader.java +│ │ ├── RegionConfigLoader.java +│ │ ├── ResetConfigLoader.java +│ │ ├── RewardConfigLoader.java +│ │ ├── TeleportConfigLoader.java +│ │ ├── UIConfigLoader.java +│ │ └── WorldGuardConfigLoader.java +│ ├── MineConfig.java +│ └── ConfigLoader.java # 427 → 151 lines +├── handler/ +│ ├── ActionBarHandler.java +│ ├── MineBlockHandler.java +│ ├── MineResetHandler.java +│ └── MineRewardHandler.java ├── block/ │ ├── BlockSetter.java │ ├── BukkitBlockSetter.java │ ├── OraxenBlockSetter.java.disabled │ └── ItemsAdderBlockSetter.java.disabled -│ -├── handler/ -│ ├── reset/ -│ │ ├── MineResetHandler.java # ≤150 (split from 209) -│ │ └── PlayerTeleportHandler.java # NEW -│ ├── block/ -│ │ └── MineBlockHandler.java -│ ├── reward/ -│ │ └── MineRewardHandler.java -│ └── ui/ -│ └── ActionBarHandler.java -│ ├── listener/ -│ ├── block/ -│ │ └── BlockBreakListener.java -│ ├── gui/ -│ │ ├── MineEditGuiListener.java # ≤150 (split from 173) -│ │ ├── GuiActionHandler.java # NEW -│ │ └── GroupGuiListener.java -│ └── player/ -│ ├── PlayerJoinListener.java -│ └── PlayerInteractListener.java -│ +│ ├── BlockBreakListener.java +│ ├── PlayerInteractListener.java +│ ├── PlayerJoinListener.java +│ └── GroupGuiListener.java +├── util/ +│ ├── ValidationUtils.java +│ ├── ErrorHandler.java +│ ├── MessageFormatter.java +│ ├── block/ ← ✅ NEW: Block utilities +│ │ └── BlockUpdateUtil.java # Fix ghost blocks, safe teleport +│ ├── format/ +│ │ ├── ChunkUtils.java +│ │ └── TimeFormatter.java +│ ├── location/ +│ │ ├── BlockKeys.java +│ │ ├── Cuboid.java +│ │ └── LocationParser.java +│ └── selection/ +│ ├── MaskScanner.java +│ ├── Selection.java +│ └── SelectionManager.java +├── wand/ +│ ├── GroupWandItem.java +│ ├── GroupWandManager.java +│ └── GroupWandSession.java ├── gui/ -│ ├── common/ -│ │ └── ItemStackFactory.java -│ ├── confirm/ -│ │ ├── ConfirmDeleteGui.java -│ │ └── ConfirmDeleteGuiHolder.java -│ ├── group/ -│ │ ├── GroupCreateGui.java -│ │ ├── GroupCreateGuiHolder.java -│ │ └── GroupCreateItems.java -│ └── mine/ -│ ├── main/ -│ │ ├── MineEditGui.java -│ │ ├── MineEditItems.java # ≤150 (split from 190) -│ │ └── MineEditItemActions.java # NEW -│ ├── edit/ -│ │ ├── blocks/ -│ │ │ ├── BlocksGui.java # ≤150 (split from 151) -│ │ │ ├── BlockWeightEditor.java # NEW -│ │ │ ├── BlocksGuiItems.java -│ │ │ ├── BlockMaterialSelectionGui.java # ≤150 (split from 180) -│ │ │ └── BlockMaterialSelector.java # NEW -│ │ ├── reset/ -│ │ │ ├── ResetGui.java -│ │ │ └── ResetGuiItems.java -│ │ └── rewards/ -│ │ └── RewardsGui.java -│ └── holder/ -│ ├── main/ -│ │ └── MineEditGuiHolder.java -│ └── edit/ -│ ├── BlocksGuiHolder.java -│ ├── ResetGuiHolder.java -│ ├── RewardsGuiHolder.java -│ └── BlockMaterialSelectionGuiHolder.java -│ +│ ├── GroupCreateGui.java +│ └── GroupCreateGuiHolder.java ├── integration/ │ ├── IntegrationManager.java -│ ├── placeholder/ +│ ├── placeholder/ # ✅ NEW: PlaceholderAPI │ │ └── LoMinesPlaceholderExpansion.java │ ├── worldguard/ -│ │ ├── config/ -│ │ │ ├── WorldGuardConfig.java # ≤150 (split from 154) -│ │ │ └── RegionTemplateConfig.java # NEW -│ │ └── service/ -│ │ ├── WorldGuardRegionService.java # ≤150 (split from 163) -│ │ ├── RegionTemplateRenderer.java # NEW -│ │ ├── WorldGuardFlagParser.java -│ │ └── WorldGuardMemberHandler.java -│ └── hologram/ -│ ├── HologramManager.java # ≤150 (split from 203) -│ ├── HologramRenderer.java # NEW -│ ├── HologramProvider.java -│ └── provider/ -│ ├── HolographicDisplaysProvider.java -│ └── DecentHologramsProvider.java -│ -├── wand/ -│ ├── WandParticleService.java -│ ├── ParticleUtil.java -│ └── group/ -│ ├── GroupWandItem.java -│ ├── GroupWandManager.java -│ └── GroupWandSession.java -│ -└── util/ - ├── package-info.java - ├── ValidationUtils.java - ├── ErrorHandler.java - ├── MessageFormatter.java - ├── block/ - │ ├── BlockUpdateUtil.java # ≤150 (split from 160) - │ └── SafeTeleportFinder.java # NEW - ├── format/ - │ ├── ColorUtils.java # ≤150 (split from 207) — фасад - │ ├── LegacyColorConverter.java # NEW - │ ├── HexColorConverter.java # NEW - │ ├── ChunkUtils.java # ≤150 (split from 174) - │ ├── ChunkRefresher.java # NEW - │ └── TimeFormatter.java - ├── location/ - │ ├── BlockKeys.java - │ ├── Cuboid.java # ≤150 (split from 181) - │ ├── CuboidSerializer.java # NEW - │ ├── LocationParser.java - │ └── SafeTeleportUtil.java - └── selection/ - ├── MaskScanner.java - ├── Selection.java - └── SelectionManager.java +│ │ ├── WorldGuardConfig.java # Шаблоны имён регионов, флаги +│ │ ├── WorldGuardFlagParser.java # Парсинг флагов WG +│ │ └── WorldGuardRegionService.java # Создание/удаление регионов +│ └── PlaceholderAPIIntegration.java.disabled +└── data/stats/ + ├── StatsManager.java + ├── PlayerStats.java + ├── Leaderboard.java + └── LeaderboardEntry.java ``` --- -## 📐 Правила (новые лимиты) +## 📝 Правила разработки -| Метрика | Лимит | -|---------|-------| -| **Файл** | ≤150 строк | -| **Папка** | ≤3 файла (package-info не считается) | -| **Метод** | ≤30 строк | -| **Вложенность** | ≤3 уровня | -| **Импорты** | No wildcard, no unused | -| **Отступы** | 4 пробела | -| **Javadoc** | Обязателен для public API | +### Лимиты строк +- **Главный класс плагина:** ≤120 строк +- **Обычные классы:** ≤200 строк +- **Утилиты:** ≤150 строк +- **Секции конфигурации:** ≤100 строк + +### Принципы +- **KISS:** Один метод = одна задача, вложенность ≤3 уровней +- **DRY:** Дублирование кода ≥3 раз → вынести в метод/класс +- **SOLID:** Каждый класс = одна ответственность +- **YAGNI:** Не добавлять функциональность "на будущее" --- -## 🔧 Checkstyle +## 📊 Метрики качества -Будет добавлен `config/checkstyle/checkstyle.xml` с правилами: -- `FileLength` — max 150 строк -- `MethodLength` — max 30 строк -- `LineLength` — max 150 символов -- `AvoidStarImport` — запрет * -- `UnusedImports` — проверка -- `Indentation` — 4 spaces -- `JavadocType`, `JavadocMethod` — для public -- `HideUtilityClassConstructor` — утилиты с private constructor -- `OneTopLevelClass` — один класс на файл +### Конфигурация (новая система) +- Средний размер секции: **~60 строк** ✅ +- Количество полей на секцию: **≤8** ✅ +- Валидация: **Constructor-time** ✅ +- Типобезопасность: **Sealed interfaces + Records** ✅ -Плагин в Gradle: `id("checkstyle") version "latest"` (built-in) +### Текущие (весь проект) +- Средний размер класса: **~120 строк** ✅ +- Классов >200 строк: **~3%** (GUI классы — допустимо) ✅ +- Дублирование кода: **<5%** ✅ + +### Новые лоадеры конфигурации +- `BlockConfigLoader` — парсинг блоков и весов +- `RegionConfigLoader` — парсинг регионов из локаций +- `ResetConfigLoader` — парсинг настроек сброса +- `RewardConfigLoader` — парсинг наград +- `TeleportConfigLoader` — парсинг телепорта +- `UIConfigLoader` — парсинг UI настроек +- `WorldGuardConfigLoader` — парсинг WG интеграции --- -## 📊 Метрики после реструктуризации +## 🐛 Технический долг -| Метрика | Сейчас | Цель | Статус | -|---------|--------|------|--------| -| Файлов >150 строк | **0** | 0 | ✅ | -| Папок >3 файлов | **0** | 0 | ✅ | -| Всего файлов | **145 main + 31 test** | ~160 main | ✅ | -| Средняя длина файла | **~75** | ~75 | ✅ | -| Checkstyle | **✅ Есть** | ✅ | ✅ | +### 🔴 Критический (нужно сделать) +- [x] Обновить Mine.java для использования новой конфигурации ✅ +- [x] Обновить MineFileManager.java для нового ConfigLoader ✅ + +### 🟡 Средний +- [ ] Включить интеграции с Oraxen и ItemsAdder +- [x] GUI под-редакторы (BlocksGui, ResetGui, RewardsGui) ✅ +- [x] PlaceholderAPI интеграция ✅ + +### 🟢 Низкий +- [ ] Добавить `/lm info ` — детальная информация +- [ ] Добавить `/lm tp ` — телепортация +- [ ] Добавить `/lm copy ` — копирование +- [x] Разбить большие GUI файлы (>200 строк) ✅ --- -## ✅ Реструктуризация завершена (2026-07-06) +## 📦 Git история -### Phase 1: Checkstyle -- `config/checkstyle/checkstyle.xml` — Google-based: 4 пробела, 150 строк/файл, 30 строк/метод, 150 символов/строка -- `build.gradle.kts` — `checkstyle` плагин + `toolVersion = "13.7.0"` - -### Phase 2: Split main файлов >150 строк -- Создано **18 новых классов**, 0 переполненных файлов осталось - -### Phase 3: Разбивка папок (≤3 файла) -- 13 переполненных папок → подпапки -- Все package declarations и импорты обновлены - -### Phase 4: Split тестов >150 строк -- 11 тестов → 25 файлов, все ≤150 строк - -### Phase 5: Импорты -- RegistrationManager, ComponentInitializer обновлены - -### Phase 6: Финальная проверка -- **0 файлов >150 строк** -- **0 папок >3 файлов** -- **145 main + 31 test = 176 файлов** +- `1daff28` - feat: PlaceholderAPI integration with mine and player placeholders +- `cfaf638` - feat: commands to set teleport and spawn locations from player position +- `bb2e47a` - feat: player-spawn config for stuck players, limit teleport height +- `0612465` - fix: ghost blocks and safe teleport location +- `3ba6f30` - feat: WorldGuard auto-region creation, split ConfigLoader +- `23d527e` - docs: update TODO with WorldGuard progress +- `75d7851` - feat: GUI editors (MineEditGui, ConfirmDeleteGui), tab completer +- `f1060f3` - refactor(config): complete rewrite of configuration system +- `f4e9458` - refactor: migrate package from com.loki to dev.loki +- `41924ea` - refactor: simplify LoMinesPlugin (220→109 lines) +- `1e29889` - refactor: split MineConfig and Mines classes +- `14daef3` - refactor: reorganize data/ and util/ into subpackages +- `2dc2543` - refactor: split MineCommands into separate command classes +- `5ddd6e9` - refactor: split ConfigParser into 3 classes +- `bd32b9f` - chore: initial commit - LoMines v3.0.0 base structure --- -## ✅ Постреструктуризационные задачи завершены (2026-08-12) - -### Docs -- `ARCHITECTURE.md` полностью переписан под реальный код: `dev.loki.lomines`, `extends JavaPlugin`, без lolib/LoAPI (ранее описывал Kotlin-версию) -- Удалён устаревший `core/common/package-info.java` (ссылался на `LoPlugin`, `Mines`/`Mine` в `core.common`) — пакет был пустым - -### Checkstyle -- `BeforeExecutionExclusionFileFilter` (исключавший `src/test`) убран — тесты теперь проверяются -- `MethodLength` 80 → 30 -- Исправлены нарушения: 23 в main (выделение private-методов) + 60 в test (unused imports, camelCase для тестов) - -### Тесты: MockBukkit -- Подключён `org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.110.0` (testImplementation) -- `testImplementation` paper-api приведён к `1.21.11-R0.1-SNAPSHOT` (требование MockBukkit), `compileOnly` остаётся `1.21.4` -- Включены все 48 ранее отключённых тестов: `BlockKeyTest`, `BlockConfigTest`, `MineTest`, `MineConfigTest`, `MineConfigValidationTest`, `MinesTest`, `MinesLoadingTest`, `MinesLifecycleTest` -- **Итог: 184 теста, 0 skipped, 0 failures** - -### Багфикс -- `Mines.create("TestMine")` сохранял `testmine.yml` (record нормализует имя в lowercase), но загружал `TestMine.yml` → `IOException`. Исправлено нормализацией имени в `Mines.create` (`name.toLowerCase().trim()`) - -### Cleanup -- Удалены `bin/` (Eclipse output, был gitignored) и `libs/` (содержал только `.gitkeep`) +*Последнее обновление: 2026-05-31 +Добавлена настройка телепорта застрявших игроков!* 🎉