From e8d516020c819e18894bef19a1011d54888be38c Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Wed, 12 Aug 2026 13:59:33 +0200 Subject: [PATCH] chore: rewrite ARCHITECTURE, enable tests via MockBukkit, checkstyle for tests, cleanup bin/libs --- ARCHITECTURE.md | 970 ++++++------------ build.gradle.kts | 6 +- config/checkstyle/checkstyle.xml | 6 +- libs/.gitkeep | 0 .../command/admin/info/InfoCommand.java | 20 +- .../admin/info/RegionActionHandler.java | 32 +- .../command/admin/info/RegionCommands.java | 98 +- .../command/admin/manage/AdminCommands.java | 92 +- .../command/admin/manage/CopyCommand.java | 45 +- .../admin/misc/TeleportActionHandler.java | 63 +- .../admin/stats/LeaderboardRenderer.java | 72 +- .../command/admin/stats/StatsCommands.java | 62 +- .../command/player/TeleportCommand.java | 31 +- .../lomines/core/common/package-info.java | 11 - .../lomines/core/mine/registry/Mines.java | 1 + .../loader/block/BlockConfigLoader.java | 25 +- .../loader/region/WorldGuardConfigLoader.java | 84 +- .../loader/reward/RewardConfigLoader.java | 30 +- .../data/reward/parse/RewardParser.java | 43 +- .../data/stats/service/StatsPersistence.java | 50 +- .../lomines/gui/group/GroupCreateGui.java | 23 +- .../edit/blocks/edit/BlockWeightEditor.java | 35 +- .../lomines/gui/mine/edit/reset/ResetGui.java | 21 +- .../lomines/gui/mine/main/MineEditGui.java | 28 +- .../region/WorldGuardRegionService.java | 40 +- .../player/PlayerInteractListener.java | 54 +- .../lomines/util/location/LocationParser.java | 4 + .../util/location/safe/SafeTeleportUtil.java | 4 + .../lomines/command/AdminCommandsTest.java | 3 - .../java/dev/loki/lomines/core/MineTest.java | 26 +- .../dev/loki/lomines/core/MineTickerTest.java | 6 +- .../loki/lomines/core/MinesLifecycleTest.java | 47 +- .../loki/lomines/core/MinesLoadingTest.java | 43 +- .../java/dev/loki/lomines/core/MinesTest.java | 43 +- .../lomines/data/LeaderboardPositionTest.java | 6 +- .../lomines/data/LeaderboardQueryTest.java | 6 +- .../loki/lomines/data/LeaderboardTest.java | 14 +- .../data/StatsManagerConcurrencyTest.java | 2 - .../loki/lomines/data/StatsManagerIOTest.java | 1 - .../loki/lomines/data/StatsManagerTest.java | 1 - .../lomines/data/config/MineConfigTest.java | 156 ++- .../data/config/MineConfigValidationTest.java | 87 +- .../data/config/block/BlockConfigTest.java | 18 +- .../data/config/block/BlockKeyTest.java | 18 +- .../lomines/data/config/ui/UIConfigTest.java | 1 - .../lomines/util/ChunkUtilsRefreshTest.java | 5 +- .../dev/loki/lomines/util/ChunkUtilsTest.java | 9 +- .../lomines/util/LocationParserErrorTest.java | 2 - .../loki/lomines/util/TimeFormatterTest.java | 2 - .../util/selection/SelectionBoundsTest.java | 1 - .../util/selection/SelectionManagerTest.java | 24 +- .../lomines/util/selection/SelectionTest.java | 3 - todo.md | 29 +- 53 files changed, 1093 insertions(+), 1410 deletions(-) delete mode 100644 libs/.gitkeep delete mode 100644 src/main/java/dev/loki/lomines/core/common/package-info.java diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 46d3c16..c62cb28 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,24 +1,25 @@ -# ARCHITECTURE.md — LoMines (Java Rewrite) +# ARCHITECTURE.md — LoMines (Java, Paper) -> **Лицензия:** 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 API. -Данный документ описывает архитектуру **Java-версии** (переписана с Kotlin). -В качестве базовой библиотеки используется **LoAPI (lolib 2.0.1)**. +LoMines — Minecraft-плагин системы шахт для Paper 1.19.2+ (сборка на API 1.21.4, Java 21). +Плагин написан на чистом Java без сторонних minecraft-фреймворков и реализует: -Плагин реализует: -- Управление шахтами (создание, удаление, сброс, конфигурация) -- Мультирегиональные шахты (до 10 `Cuboid`-регионов, по 5 пар точек) -- Быструю/стандартную установку блоков с поддержкой Oraxen и ItemsAdder -- Систему наград при добыче блоков -- Статистику игроков и лидерборды -- Интеграции с PlaceholderAPI и WorldGuard -- GUI-редактор шахт -- Визуализацию выделения частицами +- Управление шахтами (создание, удаление, сброс, перезагрузка, конфигурация) +- Мультирегиональные шахты (до 5 пар точек → 5 `Cuboid`-регионов) +- Два режима заполнения: `CUBOID` (весь регион) и `MASK` (только по меткам) +- Взвешенные блоки (шансы нормализуются автоматически) +- Авто-сбросы: по интервалу (таймер) или по проценту добытых блоков +- WorldGuard-интеграцию (авто-создание/обновление/удаление регионов) +- GUI-редактор шахт (main/confirm/group) +- Статистику игроков и лидерборды (кэшируемые) +- PlaceholderAPI-интеграцию +- Голограммы (DecentHolograms / HolographicDisplays — провайдеры) +- Групповую палку для массового создания шахт, wand-частицы --- @@ -26,720 +27,338 @@ LoMines — Minecraft-плагин системы шахт, основанный | Зависимость | Scope | Откуда / Зачем | |---|---|---| -| `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) | +| `paper-api:1.21.4-R0.1-SNAPSHOT` | compileOnly | Paper API | +| `commons-math3:3.6.1` | implementation | `EnumeratedDistribution` — взвешенный рандом | +| `commons-io:2.18.0` | implementation | утилиты для работы с файлами | | `placeholderapi:2.11.6` | compileOnly | PlaceholderAPI | -| `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 тесты | +| `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 в тестах | -> LoAPI шейдится в JAR плагина. Relocate: `dev.lolib` → `com.loki.lomines.libs.lolib` +> Сборка: Gradle + shadow (`build/libs/LoMines-3.0.0.jar`). Проверка качества: Checkstyle (`config/checkstyle/checkstyle.xml`), severity=error. --- ## Структура пакетов ``` -com.loki.lomines/ +dev.loki.lomines/ │ -├── LoMinesPlugin.java # extends LoPlugin (LoAPI) +├── LoMinesPlugin.java # главный класс, extends JavaPlugin +├── ComponentInitializer.java # создание компонентов, загрузка шахт, тикер, статистика +├── RegistrationManager.java # регистрация команд, слушателей, интеграций │ -├── 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 +├── 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/ │ └── stats/ -│ └── StatsCommands.java # stats, top, stats reset +│ ├── model/ # PlayerStats, Leaderboard, LeaderboardEntry +│ └── service/ # StatsManager, StatsPersistence │ -├── config/ -│ └── impl/ -│ ├── Config.java # config.yml — глобальные настройки -│ ├── Messages.java # messages.yml — локализация -│ └── MineConfig.java # mines/.yml — конфиг шахты +├── block/ +│ ├── BlockSetter.java # абстрактный setter (fill / fillAtLocations) +│ ├── BukkitBlockSetter.java # vanilla через Bukkit API (async fill) +│ ├── OraxenBlockSetter.java.disabled # отключено (внешние плагины выпилены) +│ └── ItemsAdderBlockSetter.java.disabled │ -├── 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 +├── handler/ +│ ├── reset/ # MineResetHandler, PlayerTeleportHandler +│ ├── block/ # MineBlockHandler +│ ├── reward/ # MineRewardHandler +│ └── ui/ # ActionBarHandler │ ├── listener/ -│ └── BlockListener.java # BlockBreakEvent, PlayerInteractEvent, PlayerJoinEvent +│ ├── block/ # BlockBreakListener +│ ├── gui/ # MineEditGuiListener, GuiActionHandler, GroupGuiListener +│ └── player/ # PlayerInteractListener, PlayerJoinListener │ -├── converters/ -│ └── CataMinesConverter.java # Конвертация из CataMines +├── 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/) │ -└── utils/ - ├── ChunkUtils.java # sendMultiBlockChange / sendBlockChange по чанкам - ├── FileUtils.java # Пути к папке плагина, копирование ресурсов - ├── NotificationManager.java # Отключение уведомлений — ConcurrentHashSet + yaml - └── TimeUtils.java # Форматирование времени — TimeFormatter из LoAPI +├── 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 ``` --- -## Использование LoAPI +## Жизненный цикл плагина -### Главный класс — `LoPlugin` +``` +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) -`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(); - } -} +LoMinesPlugin.onDisable() + ├── mineTicker.stop() + ├── mines.getAll().forEach(Mine::stop) # отмена actionbar task + ├── statsManager.stopAutoSave() + save() # stats.yml + ├── integrationManager.shutdown() # unregister PAPI + ├── wandParticleService.stopAll() + └── hologramManager.shutdown() ``` ---- - -### 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с" - }; -} -``` +Все компоненты создаются в `ComponentInitializer` и прокидываются через конструкторы — инъекция зависимостей вручную, без DI-фреймворка. --- ## Ключевые классы -### `Mine.java` +### `Mine` (`core/mine/model/Mine.java`) -Координирует handler-ы. Никакой логики — только делегирование. +Координатор одной шахты. Бизнес-логика делегирована в handler-ы: -```java -public class Mine { - public final String name; - public final MineConfig config; - public final List cuboids; // до 5 кубоидов +| Метод | Делегирует | +|---|---| +| `reset(silent)` | `MineResetHandler.reset` | +| `onBlockBreak(player, block)` | `MineBlockHandler.handle` | +| `contains(location)` / `appliesToBlock(location)` | `RegionConfig` / `MineState` | +| `start()` / `stop()` | actionbar task | - private final MineBlockHandler blockHandler; - private final MineResetHandler resetHandler; - private final MineRewardHandler rewardHandler; - private final MineActionBarHandler actionBarHandler; - private final MineConfigLoader configLoader; +Поля: `name`, `regions` (List), `config` (MineConfig), `blockSetter`, `state` (MineState), `resetHandler`, `blockHandler`, `actionBarHandler`, `maskBlockKeys`. - private BlockSetter placer; - public double volume; // суммарный объём всех кубоидов - public double blocks; // текущее кол-во неломаных блоков - private long tick; // тик с последнего сброса +### `MineState` (`core/mine/model/MineState.java`) - 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 -} -``` +Потокобезопасное состояние шахты: +- `AtomicInteger blocks`, `AtomicInteger ticks` +- `totalVolume` — для MASK = число позиций маски, для CUBOID = объём регионов +- `getPercentFilled()` = `blocks / totalVolume * 100` -### `Mines.java` +### `Mines` (`core/mine/registry/Mines.java`) -```java -public final class Mines { - private static final Map TYPES = new HashMap<>(); - // ключ = mine.name.toLowerCase() +Фасад над реестром и сервисами: `MineFileManager` (файлы), `MineRepository` (in-memory), `MineLoader` (загрузка), `MaskScanService`, `WorldGuardRegionService`, `MineFinder` (поиск). Методы: `loadAll`, `create`, `delete`, `get/find/getAll/findByLocation`, `reloadMine`, `updateMineConfig`, `scanAndSaveMask`. - 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() { ... } -} -``` +### `MineRepository` (`core/service/MineRepository.java`) -### `BlockSetter.java` +`ConcurrentHashMap` (ключ — имя в lowercase). `createAndStart(name, config)` → `new Mine` + `add` + `start`. `reload` = stop → loadConfig → createAndStart. -```java -public abstract class BlockSetter { - protected final World world; - protected final EnumeratedDistribution distribution; // commons-math3 +### `MineFileManager` (`core/service/MineFileManager.java`) - public abstract void fill(Cuboid cuboid, IntConsumer consumer); - // consumer.accept(blocksPlaced) — вызывается после завершения заполнения -} -``` +Файловые операции в `plugins/LoMines/mines/`: `ensureFolderExists`, `createDefaultConfig`, `loadConfig`, `saveConfig`, `deleteConfig`, `saveMaskPositions`. Оборачивает `ConfigLoader` + `MineConfigWriter`. -Выбор реализации в `MineConfigLoader.createBlockSetter()`: -- ключ в `contents` содержит `oraxen:` → `OraxenBlockSetter` -- ключ содержит `itemsadder:` → `ItemsAdderBlockSetter` -- иначе → `BukkitBlockSetter` (vanilla) +### `MineTicker` (`core/mine/service/MineTicker.java`) -### `MineResetHandler.java` +Один общий `BukkitTask` (1L, 1L) тикает все шахты. Если `ticks >= reset.intervalTicks()` → сброс и обнуление счётчика. -```java -public class MineResetHandler { - private final AtomicBoolean running = new AtomicBoolean(false); +### `MineResetHandler` (`handler/reset/MineResetHandler.java`) - 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) +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) ``` +### `MineBlockHandler` (`handler/block/MineBlockHandler.java`) + +`handle(player, block)`: +1. `blocks.decrementAndGet()` +2. `rewardHandler.checkRewards(player, block)` +3. статистика: `statsManager.incrementBlocks(uuid, mineName)` +4. если `percentEnabled && percent <= percentTrigger` → `mine.reset(false)` + +### `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) +) +``` + +Валидация и дефенсивные копии — в компакт-конструкторе каждого record. Парсинг/сохранение — через `ConfigLoader` с подлоадерами по секциям (`loader/block`, `loader/region`, ...). `DefaultsMerger` заполняет отсутствующие ключи из `_defaults.yml`. + +Формат локаций в yaml: `мир;x;y;z;yaw;pitch` (yaw/pitch можно опустить). Регионы — пары точек `region.selection.1` + `region.selection.2` = один Cuboid. + +### WorldGuard + +`WorldGuardRegionService` создаёт/обновляет/удаляет `ProtectedCuboidRegion` при операциях с шахтой. Имя региона — шаблон `{mine_name}_{random_4}` (см. `RegionTemplateConfig`). Флаги — через `WorldGuardFlagParser` (любые: `block-break=allow`, `pvp=deny`, ...), владельцы/участники — через `WorldGuardMemberHandler`. Всё активируется только если WorldGuard присутствует и `worldguard.enabled: true`. + --- ## Поток данных при разрушении блока ``` BlockBreakEvent (MONITOR, ignoreCancelled=true) - └── 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) + └── 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) ``` --- -## Поток сброса шахты - -``` -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 -├── ConcurrentHashMap stats -├── load() ← enable() -├── save() ← по таймеру Scheduler.runTimerAsync + disable() -└── incrementStats(Player, Mine) - ├── if !Config.STATISTICS_ENABLED → return - └── stats.computeIfAbsent(uuid) → incrementTotal() + incrementMine(mine.name) +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() -Leaderboard (stateless) -├── getTopTotal(limit) → сортировка по totalBlocks desc -├── getTopByMine(mine, limit) → фильтр + сортировка по getMineStat(mine) desc -├── getPosition(uuid) → 1-based, -1 если нет -└── getPositionByMine(uuid, mine) +Leaderboard (data/stats/model) # кэшируемый топ +├── getTopTotal(limit) → кэш + double-checked locking +├── getTopByMine(mine, limit) → без кэша +└── getPosition(uuid) → 1-based позиция ``` +`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` — форматирование строк. + --- ## Принципы @@ -749,23 +368,18 @@ Leaderboard (stateless) - `BlockSetter` — новые реализации добавляются наследованием (OCP) - Все `BlockSetter` взаимозаменяемы в `MineResetHandler` (LSP) - Handler-классы зависят только от нужного (ISP) -- `Mine` зависит от абстракции `BlockSetter`, не от конкретного NMS (DIP) +- `Mine` зависит от абстракции `BlockSetter`, не от конкретной реализации (DIP) **Ограничения на код:** -- Максимум 200 строк на файл (кроме сгенерированных/библиотечных) -- Асинхронность — только через `Scheduler.get(plugin)`, никогда `new Thread()` -- Bukkit API — только в main thread (кроме async-save статистики) -- Изменяемое static состояние — только в `Mines.TYPES` и `StatsManager.stats` +- Максимум 150 строк на файл, 30 строк на метод (план) — фактически проверяется Checkstyle (см. `config/checkstyle/checkstyle.xml`) +- Асинхронность — только через `Bukkit.getScheduler()`, никогда `new Thread()` напрямую +- Bukkit API — только в main thread (кроме async-fill и async-save статистики) +- Внутренние зависимости — конструкторы, без static-синглтонов --- -## Известные проблемы +## Тесты -| # | Проблема | Файл | Приоритет | -|---|---|---|---| -| 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` | низкий | +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`. diff --git a/build.gradle.kts b/build.gradle.kts index 04b0915..1dd9a63 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,9 +41,13 @@ dependencies { 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("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + 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 { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index a3a9089..946b028 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -8,10 +8,6 @@ - - - - @@ -24,7 +20,7 @@ - + diff --git a/libs/.gitkeep b/libs/.gitkeep deleted file mode 100644 index e69de29..0000000 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 index ef6c9a5..2c82737 100644 --- a/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java +++ b/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java @@ -3,6 +3,7 @@ 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; @@ -40,8 +41,16 @@ public class InfoCommand { 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Регионы:")); - List regions = mine.getRegions(); for (int i = 0; i < regions.size(); i++) { Cuboid r = regions.get(i); sender.sendMessage(Component.text("§7 #" + (i + 1) + ": §f" + @@ -50,8 +59,10 @@ public class InfoCommand { " §7(§f" + r.getVolume() + " §7блоков)")); } sender.sendMessage(Component.text("")); + } + + private void printBlocks(CommandSender sender, Map weights) { sender.sendMessage(Component.text("§6§lБлоки:")); - Map weights = config.blocks().weights(); weights.entrySet().stream() .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) .limit(10) @@ -60,7 +71,9 @@ public class InfoCommand { 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" + @@ -71,9 +84,6 @@ public class InfoCommand { (config.teleport().enabled() ? "§aустановлена" : "§7не задана"))); sender.sendMessage(Component.text("§7 Точка спавна: §f" + (config.playerSpawn().enabled() ? "§aустановлена" : "§7используется точка телепорта"))); - - sender.sendMessage(Component.text("§6§lНаграды: §f" + config.rewards().entries().size() + " записей")); - sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); } private String formatBlockKey(BlockKey key) { 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 index a090b93..db92fe2 100644 --- a/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java +++ b/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java @@ -33,6 +33,20 @@ public final class RegionActionHandler { 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(); @@ -40,7 +54,7 @@ public final class RegionActionHandler { var pos2 = session.getPos2(slot); if (pos1 == null || pos2 == null) { player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка")); - return; + return null; } Cuboid newRegion = new Cuboid(pos1, pos2); String mineWorld = mine.getConfig().region().worldName(); @@ -48,21 +62,17 @@ public final class RegionActionHandler { if (!mineWorld.equals(regionWorld)) { player.sendMessage(Component.text("§cРегион должен быть в мире §f" + mineWorld + "§c, а не §f" + regionWorld)); - return; + return null; } + session.clearCorners(slot); + return newRegion; + } - List currentRegions = new ArrayList<>(mine.getRegions()); - currentRegions.add(newRegion); - - var newRegionConfig = new RegionConfig(currentRegions); - updateMineRegions(mine, newRegionConfig); - - player.sendMessage(Component.text("§aДобавлен регион #" + currentRegions.size())); + 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блоков")); - - session.clearCorners(slot); } public void removeRegion(CommandSender sender, String mineName, int index) { 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 index 34cc9f4..95fca1f 100644 --- a/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java @@ -21,53 +21,59 @@ public class RegionCommands { public void handle(CommandSender sender, String subcommand, String[] args) { switch (subcommand.toLowerCase()) { - case "regions" -> { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm regions ", 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━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); - } - case "addregion" -> { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm addregion ", NamedTextColor.RED)); - return; - } - actionHandler.addRegion(sender, args[0]); - } - case "removeregion" -> { - 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)); - } - } + 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 index 46d950b..1f51a8d 100644 --- a/src/main/java/dev/loki/lomines/command/admin/manage/AdminCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/manage/AdminCommands.java @@ -25,52 +25,62 @@ public class AdminCommands { public void handle(CommandSender sender, String subcommand, String[] args) { switch (subcommand.toLowerCase()) { - case "create" -> { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm create ")); - return; - } - actionHandler.create(sender, args[0]); - } - case "delete" -> { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm delete ")); - return; - } - actionHandler.delete(sender, args[0]); - } - case "edit" -> { - if (args.length < 1) { - sender.sendMessage(Component.text("Usage: /lm edit ")); - return; - } - actionHandler.edit(sender, args[0]); - } - case "reset" -> { - 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); - } + 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" -> { - 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)); - } - } + 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(); 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 index 6b4f198..4608feb 100644 --- a/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java +++ b/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java @@ -23,27 +23,51 @@ public class CopyCommand { 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; + return null; } Mine toMine = plugin.getMines().find(toMineName).orElse(null); if (toMine == null) { sender.sendMessage(Component.text("Целевая шахта не найдена: " + toMineName, NamedTextColor.RED)); - return; + return null; } if (fromMine.getName().equalsIgnoreCase(toMine.getName())) { sender.sendMessage(Component.text("Нельзя копировать шахту саму в себя!", NamedTextColor.RED)); - return; + return null; } + return new MinePair(fromMine, toMine); + } - var sourceConfig = fromMine.getConfig(); + private MineConfig buildCopiedConfig(Mine toMine, MineConfig sourceConfig) { var targetConfig = toMine.getConfig(); - - MineConfig newTargetConfig = MineConfig.builder(toMine.getName()) + return MineConfig.builder(toMine.getName()) .region(targetConfig.region()) .blocks(sourceConfig.blocks()) .reset(sourceConfig.reset()) @@ -53,14 +77,9 @@ public class CopyCommand { .worldGuard(sourceConfig.worldGuard()) .playerSpawn(sourceConfig.playerSpawn()) .build(); + } - try { - plugin.getMines().updateMineConfig(toMine.getName(), newTargetConfig); - } catch (Exception e) { - sender.sendMessage(Component.text("Ошибка сохранения: " + e.getMessage(), NamedTextColor.RED)); - return; - } - + 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Скопировано:")); diff --git a/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java b/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java index 412d5e7..dd974d5 100644 --- a/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java +++ b/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java @@ -40,18 +40,7 @@ public final class TeleportActionHandler { } var config = mine.getConfig(); - 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(); + var updatedConfig = withTeleport(config, TeleportConfig.at(player.getLocation())); plugin.getMines().getFileManager().saveConfig(updatedConfig); sender.sendMessage(MessageFormatter.success("Teleport location for mine '" + mineName + "' set to your current position")); @@ -76,18 +65,7 @@ public final class TeleportActionHandler { } var config = mine.getConfig(); - 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(); + var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.at(player.getLocation())); plugin.getMines().getFileManager().saveConfig(updatedConfig); sender.sendMessage(MessageFormatter.success("Safe spawn location for mine '" + mineName + "' set to your current position")); @@ -108,16 +86,7 @@ public final class TeleportActionHandler { } var config = mine.getConfig(); - 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(); + var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.disabled()); plugin.getMines().getFileManager().saveConfig(updatedConfig); sender.sendMessage(MessageFormatter.success("Spawn location for mine '" + mineName + "' cleared")); @@ -128,6 +97,32 @@ 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) { 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/stats/LeaderboardRenderer.java b/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java index dc52ff5..f86c39b 100644 --- a/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java +++ b/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java @@ -3,7 +3,6 @@ package dev.loki.lomines.command.admin.stats; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.core.mine.model.Mine; import dev.loki.lomines.core.mine.registry.Mines; -import dev.loki.lomines.data.stats.model.Leaderboard; import dev.loki.lomines.data.stats.model.LeaderboardEntry; import dev.loki.lomines.util.ErrorHandler; import net.kyori.adventure.text.Component; @@ -77,43 +76,46 @@ public final class LeaderboardRenderer { } public static void renderTopCommand(CommandSender sender, String arg1, String arg2, Mines mines, LoMinesPlugin plugin) { - String a = arg1 == null ? "" : arg1.trim(); - String b = arg2 == null ? "" : arg2.trim(); - int limit; - String mineName; - if (a.isEmpty()) { - limit = clampLimit(10); - mineName = null; - } else if (b.isEmpty()) { - Integer asLimit = tryParsePositiveInt(a); - if (asLimit != null) { - limit = clampLimit(asLimit); - mineName = null; - } else { - limit = clampLimit(10); - mineName = a; - } - } else { - mineName = a; - Integer limParsed = tryParsePositiveInt(b); - limit = clampLimit(limParsed != null ? limParsed : 10); - } - Leaderboard lb = plugin.getStatsManager().getLeaderboard(); - if (mineName != null) { - var found = mines.find(mineName); - if (found.isEmpty()) { - sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - Mine m = found.get(); - String key = m.getName(); - List rows = lb.getTopByMine(key, limit); - sender.sendMessage(Component.text("=== Топ по шахте \u00ab" + key + "\u00bb ===", NamedTextColor.GOLD)); - sendTopRows(sender, rows); + TopRequest req = parseTopRequest(arg1, arg2); + if (req.mineName() != null) { + renderMineTop(sender, req.mineName(), req.limit(), mines, plugin); return; } - List rows = lb.getTopTotal(limit); + 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 index 3a4fc71..4cd0bb0 100644 --- a/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java @@ -26,36 +26,7 @@ public class StatsCommands { public void handle(CommandSender sender, String subcommand, String[] args) { switch (subcommand.toLowerCase()) { - case "stats" -> { - if (isStatsDisabled(sender)) { - return; - } - String targetName = args.length > 0 ? args[0] : ""; - UUID uuid = LeaderboardRenderer.resolvePlayerUuid(sender, targetName, errorHandler); - if (uuid == null) { - return; - } - - StatsManager sm = plugin.getStatsManager(); - PlayerStats ps = sm.getOrCreate(uuid); - String displayName = LeaderboardRenderer.formatPlayerName(uuid); - sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD)); - sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE)); - int pos = sm.getLeaderboard().getPosition(uuid); - if (pos > 0) { - sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY)); - } - Map 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))); - } + case "stats" -> handleStats(sender, args); case "top" -> { if (isStatsDisabled(sender)) { return; @@ -68,6 +39,37 @@ public class StatsCommands { } } + 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)); diff --git a/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java b/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java index 308cb82..75d0d8d 100644 --- a/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java +++ b/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java @@ -35,19 +35,8 @@ public class TeleportCommand { return; } - var config = mine.getConfig(); - Location target; - - if (config.teleport().enabled() && config.teleport().getLocation().isPresent()) { - target = config.teleport().getLocation().get(); - } else 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; - target = new Location(region.getWorld(), centerX, centerY, centerZ); - } else { - player.sendMessage(Component.text("У шахты нет настроенной точки телепорта!", NamedTextColor.RED)); + Location target = resolveTarget(player, mine); + if (target == null) { return; } @@ -55,4 +44,20 @@ public class TeleportCommand { 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 deleted file mode 100644 index 2228c11..0000000 --- a/src/main/java/dev/loki/lomines/core/common/package-info.java +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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/registry/Mines.java b/src/main/java/dev/loki/lomines/core/mine/registry/Mines.java index 18fd512..5b55f1c 100644 --- a/src/main/java/dev/loki/lomines/core/mine/registry/Mines.java +++ b/src/main/java/dev/loki/lomines/core/mine/registry/Mines.java @@ -44,6 +44,7 @@ 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); } 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 e81e6e0..61b8e24 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 @@ -42,22 +42,23 @@ public final class BlockConfigLoader { yaml.getString("blocks.fill-mode", "CUBOID").toUpperCase() ); - // 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); + return new BlockConfig(weights, fillMode, parseMaskConfig(yaml, fillMode)); + } - Map positions = new HashMap<>(); - List posList = yaml.getStringList("blocks.mask.positions"); - for (String pos : posList) { - positions.put(pos, true); - } + 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); - mask = new BlockConfig.MaskConfig(marker, positions); + Map positions = new HashMap<>(); + List posList = yaml.getStringList("blocks.mask.positions"); + for (String pos : posList) { + positions.put(pos, true); } - return new BlockConfig(weights, fillMode, mask); + return new BlockConfig.MaskConfig(marker, positions); } /** 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 209ac99..7be99d1 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 @@ -22,44 +22,14 @@ public final class WorldGuardConfigLoader { * Supports any WorldGuard flags. Default: block-break=allow. */ public WorldGuardConfig parse(YamlConfiguration 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) { + if (!isEnabled(yaml)) { return WorldGuardConfig.disabled(); } - List owners = yaml.getStringList("worldguard.owners"); - if (owners.isEmpty()) { - owners = defaults.getStringList("worldguard.owners"); - } - - List members = yaml.getStringList("worldguard.members"); - if (members.isEmpty()) { - members = defaults.getStringList("worldguard.members"); - } - - // 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"); - } - - boolean protect = yaml.getBoolean("worldguard.protect-on-create", - defaults.getBoolean("worldguard.protect-on-create", true)); - + List owners = resolveOwners(yaml); + List members = resolveMembers(yaml); + List flags = resolveFlags(yaml); + boolean protect = resolveProtect(yaml); String tmpl = yaml.getString("worldguard.region-template", defaults.getString("worldguard.region-template", "{mine_name}_{random_4}")); @@ -73,6 +43,50 @@ public final class WorldGuardConfigLoader { .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"); + } + return flags; + } + + private boolean resolveProtect(YamlConfiguration yaml) { + return yaml.getBoolean("worldguard.protect-on-create", + defaults.getBoolean("worldguard.protect-on-create", true)); + } + /** * Saves WorldGuard configuration to YAML. */ 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 7597efe..e4b149a 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,16 +36,31 @@ 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; + + 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) map.get("blocks"); + List blockList = (List) rawBlocks; 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) map.get("items"); + List itemList = (List) rawItems; if (itemList != null) { for (Object i : itemList) { @SuppressWarnings("unchecked") @@ -53,9 +68,11 @@ public final class RewardConfigLoader { items.add(parseItemReward(itemMap)); } } + return items; + } + private List parseCommands(Object commandsObj) { List commands = new ArrayList<>(); - Object commandsObj = map.get("commands"); if (commandsObj instanceof List list) { for (Object obj : list) { if (obj != null) { @@ -63,12 +80,7 @@ public final class RewardConfigLoader { } } } - Object preventDropsObj = map.get("prevent-drops"); - boolean preventDrops = preventDropsObj instanceof Boolean b && b; - - Object chanceObj = map.get("chance"); - double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0; - return new RewardEntry(blocks, chance, items, commands, preventDrops); + return commands; } private ItemReward parseItemReward(Map map) { 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 index 3613b38..7752d2f 100644 --- a/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java @@ -29,29 +29,30 @@ public final class RewardParser { } 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 = entryParser.parseReward(rewardMap); - rewards.add(reward); - } catch (ConfigParseException e) { - throw new ConfigParseException( - "Error parsing reward at index " + i + ": " + e.getMessage(), - e - ); - } + 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/service/StatsPersistence.java b/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java index 0cd0780..936c000 100644 --- a/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java +++ b/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java @@ -43,34 +43,38 @@ public final class StatsPersistence { } for (String uuidString : playersSection.getKeys(false)) { - try { - UUID playerId = UUID.fromString(uuidString); - ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString); - - if (playerSection == null) { - continue; - } - - PlayerStats playerStats = stats.computeIfAbsent(playerId, PlayerStats::new); - - long totalBlocks = playerSection.getLong("total", 0); - playerStats.setTotalBlocks(totalBlocks); - - ConfigurationSection minesSection = playerSection.getConfigurationSection("mines"); - if (minesSection != null) { - for (String mineName : minesSection.getKeys(false)) { - long mineBlocks = minesSection.getLong(mineName, 0); - playerStats.setMineBlocks(mineName, mineBlocks); - } - } - } catch (IllegalArgumentException e) { - plugin.getLogger().warning("Invalid UUID in stats.yml: " + uuidString); - } + 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(); 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 e5f080a..fb47b3d 100644 --- a/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java +++ b/src/main/java/dev/loki/lomines/gui/group/GroupCreateGui.java @@ -94,6 +94,20 @@ 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; @@ -115,13 +129,6 @@ public final class GroupCreateGui { e.printStackTrace(); } } - - if (created == 0) { - player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED)); - return; - } - player.sendMessage(Component.text( - "Создано шахт: " + created + (skipped > 0 ? " (пропущено: " + skipped + ")" : ""), - NamedTextColor.GREEN)); + return new CreationResult(created, skipped); } } 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 index 4baa0d9..c5eec72 100644 --- 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 @@ -30,19 +30,32 @@ public final class BlockWeightEditor { Map weights = new java.util.HashMap<>(config.weights()); double currentWeight = weights.getOrDefault(blockKey, 0.0) * 100.0; - double delta = 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) { - delta = 1.0; - } else if (rightClick && shiftClick) { - delta = -1.0; - } else if (leftClick) { - delta = 5.0; - } else if (rightClick) { - delta = -5.0; + return 1.0; } + if (rightClick && shiftClick) { + return -1.0; + } + if (leftClick) { + return 5.0; + } + if (rightClick) { + return -5.0; + } + return 0; + } - double newWeight = Math.max(0, Math.min(100, currentWeight + delta)); - + 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( @@ -53,8 +66,6 @@ public final class BlockWeightEditor { "\u00a7a\u0412\u0435\u0441 \u00a7f" + blockKey.serialize() + " \u00a7a\u0438\u0437\u043c\u0435\u043d\u0435\u043d: \u00a7f" + String.format("%.1f%%", newWeight))); } - - BlocksGui.refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin); } public static BlockKey getBlockAtSlot(Inventory inv, int slot, LoMinesPlugin plugin, String mineName) { 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/reset/ResetGui.java index 5a2ce15..2885ff9 100644 --- a/src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGui.java +++ b/src/main/java/dev/loki/lomines/gui/mine/edit/reset/ResetGui.java @@ -87,14 +87,7 @@ public final class ResetGui { } if (rawSlot == SLOT_MESSAGE) { - 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; + return handleMessageSlot(plugin, player, mineName, leftClick, rightClick); } if (rawSlot == SLOT_COMMANDS && leftClick) { @@ -105,6 +98,18 @@ 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; 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 bf0607e..60386da 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 @@ -81,11 +81,11 @@ public final class MineEditGui { 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) { switch (rawSlot) { - case SLOT_BACK -> { - player.closeInventory(); - return true; - } case SLOT_BLOCKS -> { BlocksGui.open(plugin, player, mineName); return true; @@ -98,6 +98,22 @@ 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; @@ -111,10 +127,6 @@ 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; } 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 index eeed5b6..648e76f 100644 --- a/src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardRegionService.java +++ b/src/main/java/dev/loki/lomines/integration/worldguard/region/WorldGuardRegionService.java @@ -45,30 +45,34 @@ public final class WorldGuardRegionService { } String regionName = wgConfig.generateRegionName(mineName); try { - 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; + 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; diff --git a/src/main/java/dev/loki/lomines/listener/player/PlayerInteractListener.java b/src/main/java/dev/loki/lomines/listener/player/PlayerInteractListener.java index fda2c80..639f753 100644 --- a/src/main/java/dev/loki/lomines/listener/player/PlayerInteractListener.java +++ b/src/main/java/dev/loki/lomines/listener/player/PlayerInteractListener.java @@ -48,38 +48,44 @@ public final class PlayerInteractListener implements Listener { if (action == Action.LEFT_CLICK_BLOCK && event.hasBlock()) { event.setCancelled(true); - 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); + handleLeftClick(player, event.getClickedBlock()); return; } if (action == Action.RIGHT_CLICK_BLOCK && !player.isSneaking()) { event.setCancelled(true); - 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); + handleRightClick(player, event.getClickedBlock()); } } + 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. */ 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 f0e0953..9fb654e 100644 --- a/src/main/java/dev/loki/lomines/util/location/LocationParser.java +++ b/src/main/java/dev/loki/lomines/util/location/LocationParser.java @@ -42,6 +42,10 @@ 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)); diff --git a/src/main/java/dev/loki/lomines/util/location/safe/SafeTeleportUtil.java b/src/main/java/dev/loki/lomines/util/location/safe/SafeTeleportUtil.java index a6a9f78..2492096 100644 --- a/src/main/java/dev/loki/lomines/util/location/safe/SafeTeleportUtil.java +++ b/src/main/java/dev/loki/lomines/util/location/safe/SafeTeleportUtil.java @@ -40,6 +40,10 @@ 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; diff --git a/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java b/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java index 6fa3eae..2ccc173 100644 --- a/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java +++ b/src/test/java/dev/loki/lomines/command/AdminCommandsTest.java @@ -12,9 +12,6 @@ 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; diff --git a/src/test/java/dev/loki/lomines/core/MineTest.java b/src/test/java/dev/loki/lomines/core/MineTest.java index d407328..e621892 100644 --- a/src/test/java/dev/loki/lomines/core/MineTest.java +++ b/src/test/java/dev/loki/lomines/core/MineTest.java @@ -10,41 +10,47 @@ import dev.loki.lomines.util.location.geo.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.Disabled; 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.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; 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.mockito.Mockito.when; /** * Unit tests for Mine class, specifically testing BlockSetter creation logic. */ -@Disabled("Paper API RegistryAccess not available in unit tests") class MineTest { @Mock private LoMinesPlugin plugin; - @Mock + private ServerMock server; private World world; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - when(world.getName()).thenReturn("world"); + server = MockBukkit.mock(); + server.addSimpleWorld("world"); + world = server.getWorld("world"); + } + + @AfterEach + void tearDown() { + MockBukkit.unmock(); } @Test - void testCreateBlockSetter_WithOraxenPrefix_CreatesBukkitBlockSetter() { + void testCreateBlockSetterWithOraxenPrefixCreatesBukkitBlockSetter() { // Oraxen integration is disabled — constructor throws IllegalArgumentException Map blocks = new HashMap<>(); blocks.put(new BlockKey.Oraxen("custom_ore"), 50.0); @@ -59,7 +65,7 @@ class MineTest { } @Test - void testCreateBlockSetter_WithItemsAdderPrefix_CreatesBukkitBlockSetter() { + void testCreateBlockSetterWithItemsAdderPrefixCreatesBukkitBlockSetter() { // ItemsAdder integration is disabled — constructor throws IllegalArgumentException Map blocks = new HashMap<>(); blocks.put(new BlockKey.ItemsAdder("custom_block"), 60.0); @@ -73,7 +79,7 @@ class MineTest { } @Test - void testCreateBlockSetter_WithoutPrefix_CreatesBukkitBlockSetter() { + 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); @@ -89,7 +95,7 @@ class MineTest { } @Test - void testCreateBlockSetter_WithEmptyBlocks_ThrowsException() { + void testCreateBlockSetterWithEmptyBlocksThrowsException() { Map blocks = new HashMap<>(); assertThrows( diff --git a/src/test/java/dev/loki/lomines/core/MineTickerTest.java b/src/test/java/dev/loki/lomines/core/MineTickerTest.java index 1ee0df7..4789be2 100644 --- a/src/test/java/dev/loki/lomines/core/MineTickerTest.java +++ b/src/test/java/dev/loki/lomines/core/MineTickerTest.java @@ -29,13 +29,13 @@ class MineTickerTest { } @Test - void testConstruction_WithValidParameters_DoesNotThrow() { + void testConstructionWithValidParametersDoesNotThrow() { // Act & Assert assertDoesNotThrow(() -> new MineTicker(mines, plugin)); } @Test - void testStop_WhenNotRunning_DoesNotThrow() { + void testStopWhenNotRunningDoesNotThrow() { // Arrange MineTicker ticker = new MineTicker(mines, plugin); @@ -44,7 +44,7 @@ class MineTickerTest { } @Test - void testMultipleStops_DoNotThrow() { + void testMultipleStopsDoNotThrow() { // 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 index aa42d39..b94c6cf 100644 --- a/src/test/java/dev/loki/lomines/core/MinesLifecycleTest.java +++ b/src/test/java/dev/loki/lomines/core/MinesLifecycleTest.java @@ -3,19 +3,12 @@ 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.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; -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.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.mockito.MockedStatic; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import java.io.IOException; import java.nio.file.Files; @@ -23,19 +16,13 @@ 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.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.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@Disabled("WorldGuard dependency not available in unit tests") class MinesLifecycleTest { @TempDir @@ -43,43 +30,25 @@ class MinesLifecycleTest { private Mines mines; private LoMinesPlugin plugin; - private BukkitScheduler scheduler; - private MockedStatic globalBukkit; + private ServerMock server; @BeforeEach void setUp() { - globalBukkit = mockStatic(Bukkit.class); - - org.bukkit.plugin.PluginManager pluginManager = mock(org.bukkit.plugin.PluginManager.class); - Server server = mock(Server.class); - World world = mock(World.class); - BlockData blockData = mock(BlockData.class); - - globalBukkit.when(Bukkit::getPluginManager).thenReturn(pluginManager); - globalBukkit.when(Bukkit::getServer).thenReturn(server); - globalBukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); - globalBukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData); - when(world.getName()).thenReturn("world"); + server = MockBukkit.mock(); + server.addSimpleWorld("world"); plugin = mock(LoMinesPlugin.class); Logger logger = mock(Logger.class); - scheduler = mock(BukkitScheduler.class); when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); when(plugin.getLogger()).thenReturn(logger); - globalBukkit.when(Bukkit::getScheduler).thenReturn(scheduler); - BukkitTask task = mock(BukkitTask.class); - when(scheduler.runTaskTimer(any(), any(Runnable.class), anyLong(), anyLong())).thenReturn(task); - mines = new Mines(plugin); } @AfterEach void tearDown() { - if (globalBukkit != null) { - globalBukkit.close(); - } + MockBukkit.unmock(); } @Test @@ -97,9 +66,9 @@ class MinesLifecycleTest { @Test void testCreateMineCreatesFolder() throws IOException { - assertFalse(Files.exists(tempDir.resolve("mines"))); - mines.create("testmine"); assertTrue(Files.isDirectory(tempDir.resolve("mines"))); + mines.create("testmine"); + assertTrue(Files.exists(tempDir.resolve("mines/testmine.yml"))); } @Test diff --git a/src/test/java/dev/loki/lomines/core/MinesLoadingTest.java b/src/test/java/dev/loki/lomines/core/MinesLoadingTest.java index 105cbd4..bf765e7 100644 --- a/src/test/java/dev/loki/lomines/core/MinesLoadingTest.java +++ b/src/test/java/dev/loki/lomines/core/MinesLoadingTest.java @@ -3,19 +3,12 @@ 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.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; -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.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.mockito.MockedStatic; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import java.io.IOException; import java.nio.file.Files; @@ -25,15 +18,9 @@ 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.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@Disabled("WorldGuard dependency not available in unit tests") class MinesLoadingTest { private static final String FULL_YAML = """ @@ -61,43 +48,25 @@ class MinesLoadingTest { private Mines mines; private LoMinesPlugin plugin; - private BukkitScheduler scheduler; - private MockedStatic globalBukkit; + private ServerMock server; @BeforeEach void setUp() { - globalBukkit = mockStatic(Bukkit.class); - - org.bukkit.plugin.PluginManager pluginManager = mock(org.bukkit.plugin.PluginManager.class); - Server server = mock(Server.class); - World world = mock(World.class); - BlockData blockData = mock(BlockData.class); - - globalBukkit.when(Bukkit::getPluginManager).thenReturn(pluginManager); - globalBukkit.when(Bukkit::getServer).thenReturn(server); - globalBukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); - globalBukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData); - when(world.getName()).thenReturn("world"); + server = MockBukkit.mock(); + server.addSimpleWorld("world"); plugin = mock(LoMinesPlugin.class); Logger logger = mock(Logger.class); - scheduler = mock(BukkitScheduler.class); when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); when(plugin.getLogger()).thenReturn(logger); - globalBukkit.when(Bukkit::getScheduler).thenReturn(scheduler); - BukkitTask task = mock(BukkitTask.class); - when(scheduler.runTaskTimer(any(), any(Runnable.class), anyLong(), anyLong())).thenReturn(task); - mines = new Mines(plugin); } @AfterEach void tearDown() { - if (globalBukkit != null) { - globalBukkit.close(); - } + MockBukkit.unmock(); } @Test diff --git a/src/test/java/dev/loki/lomines/core/MinesTest.java b/src/test/java/dev/loki/lomines/core/MinesTest.java index 2cca288..bed91d6 100644 --- a/src/test/java/dev/loki/lomines/core/MinesTest.java +++ b/src/test/java/dev/loki/lomines/core/MinesTest.java @@ -1,22 +1,13 @@ 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.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.plugin.PluginManager; -import org.bukkit.Server; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.mockito.MockedStatic; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import java.io.IOException; import java.nio.file.Files; @@ -25,19 +16,15 @@ 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.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Unit tests for Mines registry. */ -@Disabled("WorldGuard dependency not available in unit tests") class MinesTest { @TempDir @@ -45,43 +32,25 @@ class MinesTest { private Mines mines; private LoMinesPlugin plugin; - private BukkitScheduler scheduler; - private MockedStatic globalBukkit; + private ServerMock server; @BeforeEach void setUp() { - globalBukkit = mockStatic(Bukkit.class); - - PluginManager pluginManager = mock(PluginManager.class); - Server server = mock(Server.class); - World world = mock(World.class); - BlockData blockData = mock(BlockData.class); - - globalBukkit.when(Bukkit::getPluginManager).thenReturn(pluginManager); - globalBukkit.when(Bukkit::getServer).thenReturn(server); - globalBukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world); - globalBukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData); - when(world.getName()).thenReturn("world"); + server = MockBukkit.mock(); + server.addSimpleWorld("world"); plugin = mock(LoMinesPlugin.class); Logger logger = mock(Logger.class); - scheduler = mock(BukkitScheduler.class); when(plugin.getDataFolder()).thenReturn(tempDir.toFile()); when(plugin.getLogger()).thenReturn(logger); - globalBukkit.when(Bukkit::getScheduler).thenReturn(scheduler); - BukkitTask task = mock(BukkitTask.class); - when(scheduler.runTaskTimer(any(), any(Runnable.class), anyLong(), anyLong())).thenReturn(task); - mines = new Mines(plugin); } @AfterEach void tearDown() { - if (globalBukkit != null) { - globalBukkit.close(); - } + MockBukkit.unmock(); } @Test diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardPositionTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardPositionTest.java index 8830a3f..87d51e8 100644 --- a/src/test/java/dev/loki/lomines/data/LeaderboardPositionTest.java +++ b/src/test/java/dev/loki/lomines/data/LeaderboardPositionTest.java @@ -30,7 +30,7 @@ class LeaderboardPositionTest { } @Test - void testGetPosition_ReturnsCorrectPosition() { + void testGetPositionReturnsCorrectPosition() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); UUID player3 = UUID.randomUUID(); @@ -45,13 +45,13 @@ class LeaderboardPositionTest { } @Test - void testGetPosition_ReturnsMinusOneForNonExistentPlayer() { + void testGetPositionReturnsMinusOneForNonExistentPlayer() { UUID playerId = UUID.randomUUID(); assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for non-existent player"); } @Test - void testGetPosition_ReturnsMinusOneForZeroBlocks() { + void testGetPositionReturnsMinusOneForZeroBlocks() { UUID playerId = UUID.randomUUID(); statsManager.getOrCreate(playerId).setTotalBlocks(0); diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardQueryTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardQueryTest.java index 98f4cf1..0368be4 100644 --- a/src/test/java/dev/loki/lomines/data/LeaderboardQueryTest.java +++ b/src/test/java/dev/loki/lomines/data/LeaderboardQueryTest.java @@ -32,7 +32,7 @@ class LeaderboardQueryTest { } @Test - void testGetTopByMine_SortedDescending() { + void testGetTopByMineSortedDescending() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); UUID player3 = UUID.randomUUID(); @@ -49,7 +49,7 @@ class LeaderboardQueryTest { } @Test - void testGetTopByMine_FiltersZeroBlocks() { + void testGetTopByMineFiltersZeroBlocks() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); @@ -62,7 +62,7 @@ class LeaderboardQueryTest { } @Test - void testGetTopByMine_OnlyIncludesSpecificMine() { + void testGetTopByMineOnlyIncludesSpecificMine() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); diff --git a/src/test/java/dev/loki/lomines/data/LeaderboardTest.java b/src/test/java/dev/loki/lomines/data/LeaderboardTest.java index 1ab945d..4e571e1 100644 --- a/src/test/java/dev/loki/lomines/data/LeaderboardTest.java +++ b/src/test/java/dev/loki/lomines/data/LeaderboardTest.java @@ -39,13 +39,13 @@ class LeaderboardTest { } @Test - void testGetTopTotal_EmptyStats() { + void testGetTopTotalEmptyStats() { List top = leaderboard.getTopTotal(10); assertTrue(top.isEmpty(), "Top list should be empty when no stats exist"); } @Test - void testGetTopTotal_SinglePlayer() { + void testGetTopTotalSinglePlayer() { UUID playerId = UUID.randomUUID(); PlayerStats stats = statsManager.getOrCreate(playerId); stats.setTotalBlocks(100); @@ -57,7 +57,7 @@ class LeaderboardTest { } @Test - void testGetTopTotal_SortedDescending() { + void testGetTopTotalSortedDescending() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); UUID player3 = UUID.randomUUID(); @@ -74,7 +74,7 @@ class LeaderboardTest { } @Test - void testGetTopTotal_LimitRespected() { + void testGetTopTotalLimitRespected() { for (int i = 0; i < 10; i++) { UUID playerId = UUID.randomUUID(); statsManager.getOrCreate(playerId).setTotalBlocks(i + 1); @@ -85,7 +85,7 @@ class LeaderboardTest { } @Test - void testGetTopTotal_FiltersZeroBlocks() { + void testGetTopTotalFiltersZeroBlocks() { UUID player1 = UUID.randomUUID(); UUID player2 = UUID.randomUUID(); @@ -98,7 +98,7 @@ class LeaderboardTest { } @Test - void testGetTopTotal_CachingWorks() { + void testGetTopTotalCachingWorks() { UUID playerId = UUID.randomUUID(); statsManager.getOrCreate(playerId).setTotalBlocks(100); @@ -113,7 +113,7 @@ class LeaderboardTest { } @Test - void testInvalidateCache_ClearsCache() { + void testInvalidateCacheClearsCache() { UUID playerId = UUID.randomUUID(); statsManager.getOrCreate(playerId).setTotalBlocks(100); diff --git a/src/test/java/dev/loki/lomines/data/StatsManagerConcurrencyTest.java b/src/test/java/dev/loki/lomines/data/StatsManagerConcurrencyTest.java index ffa9f3b..7ee8246 100644 --- a/src/test/java/dev/loki/lomines/data/StatsManagerConcurrencyTest.java +++ b/src/test/java/dev/loki/lomines/data/StatsManagerConcurrencyTest.java @@ -1,7 +1,6 @@ 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.bukkit.Bukkit; import org.bukkit.scheduler.BukkitScheduler; @@ -11,7 +10,6 @@ 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.Path; import java.util.UUID; import java.util.logging.Logger; diff --git a/src/test/java/dev/loki/lomines/data/StatsManagerIOTest.java b/src/test/java/dev/loki/lomines/data/StatsManagerIOTest.java index 624503f..1cf4a8d 100644 --- a/src/test/java/dev/loki/lomines/data/StatsManagerIOTest.java +++ b/src/test/java/dev/loki/lomines/data/StatsManagerIOTest.java @@ -3,7 +3,6 @@ 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.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; diff --git a/src/test/java/dev/loki/lomines/data/StatsManagerTest.java b/src/test/java/dev/loki/lomines/data/StatsManagerTest.java index 00d9d00..bc1b749 100644 --- a/src/test/java/dev/loki/lomines/data/StatsManagerTest.java +++ b/src/test/java/dev/loki/lomines/data/StatsManagerTest.java @@ -8,7 +8,6 @@ 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; 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 d496a40..93c3d40 100644 --- a/src/test/java/dev/loki/lomines/data/config/MineConfigTest.java +++ b/src/test/java/dev/loki/lomines/data/config/MineConfigTest.java @@ -10,14 +10,14 @@ 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.geo.Cuboid; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import java.util.HashMap; import java.util.List; @@ -25,112 +25,96 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -@Disabled("Paper API RegistryAccess not available in unit tests") 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() { - 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); + RegionConfig region = testRegion(); + BlockConfig blocks = testBlocks(); - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 10, 74, 10); - RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); + MineConfig config = MineConfig.builder("testmine") + .region(region) + .blocks(blocks) + .reset(ResetConfig.defaults()) + .rewards(RewardConfig.empty()) + .teleport(TeleportConfig.disabled()) + .ui(UIConfig.defaults()) + .build(); - 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()); - } + assertEquals("testmine", config.name()); + assertEquals(1, config.region().regionCount()); + assertEquals(1, config.blocks().blockCount()); } @Test void testDefaultsFactory() { - 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); + RegionConfig region = testRegion(); + BlockConfig blocks = testBlocks(); - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 10, 74, 10); - RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); + MineConfig config = MineConfigDefaults.defaults("mymine", region, blocks); - Map weights = new HashMap<>(); - weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); - BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); - - MineConfig config = MineConfigDefaults.defaults("mymine", region, blocks); - - assertEquals("mymine", config.name()); - assertNotNull(config.reset()); - assertNotNull(config.rewards()); - assertNotNull(config.teleport()); - assertNotNull(config.ui()); - } + assertEquals("mymine", config.name()); + assertNotNull(config.reset()); + assertNotNull(config.rewards()); + assertNotNull(config.teleport()); + assertNotNull(config.ui()); } @Test void testNameNormalized() { - 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); + RegionConfig region = testRegion(); + BlockConfig blocks = testBlocks(); - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 10, 74, 10); - RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); + MineConfig config = MineConfig.builder("TestMine_123 ") + .region(region) + .blocks(blocks) + .build(); - 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()); - } + assertEquals("testmine_123", config.name()); } @Test void testWorldNameDelegation() { - 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); + 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(); - Location loc1 = new Location(world, 0, 64, 0); - Location loc2 = new Location(world, 10, 74, 10); - RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2)); + MineConfig config = MineConfig.builder("test") + .region(region) + .blocks(blocks) + .build(); - 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()); - } + 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 index abf2d23..26d8a3c 100644 --- a/src/test/java/dev/loki/lomines/data/config/MineConfigValidationTest.java +++ b/src/test/java/dev/loki/lomines/data/config/MineConfigValidationTest.java @@ -8,21 +8,47 @@ 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.Disabled; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; +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; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; 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, () -> @@ -30,35 +56,20 @@ class MineConfigValidationTest { } @Test - @Disabled("Paper API RegistryAccess not available in unit tests") 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); + RegionConfig region = testRegion(); + BlockConfig blocks = testBlocks(); - 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()); - } + assertThrows(IllegalArgumentException.class, () -> + MineConfig.builder(" ") + .region(region) + .blocks(blocks) + .build()); } @Test - @Disabled("Paper API RegistryAccess not available in unit tests") void testNullRegionThrows() { - Map weights = new HashMap<>(); - weights.put(new BlockKey.Vanilla(Material.STONE), 100.0); - BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + BlockConfig blocks = testBlocks(); assertThrows(NullPointerException.class, () -> MineConfig.builder("test") @@ -69,20 +80,12 @@ class MineConfigValidationTest { @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); + RegionConfig region = testRegion(); - 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()); - } + 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 6e1a6ba..48d0565 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,8 +1,11 @@ package dev.loki.lomines.data.config.block; import org.bukkit.Material; -import org.junit.jupiter.api.Disabled; +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; @@ -13,9 +16,20 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -@Disabled("Paper API RegistryAccess not available in unit tests") 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 be00498..ebf4754 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,17 +1,31 @@ package dev.loki.lomines.data.config.block; import org.bukkit.Material; -import org.junit.jupiter.api.Disabled; +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; -@Disabled("Paper API RegistryAccess not available in unit tests") 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/ui/UIConfigTest.java b/src/test/java/dev/loki/lomines/data/config/ui/UIConfigTest.java index 9453d0b..5ac6776 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 @@ -6,7 +6,6 @@ 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class UIConfigTest { diff --git a/src/test/java/dev/loki/lomines/util/ChunkUtilsRefreshTest.java b/src/test/java/dev/loki/lomines/util/ChunkUtilsRefreshTest.java index 530e646..c6d1a43 100644 --- a/src/test/java/dev/loki/lomines/util/ChunkUtilsRefreshTest.java +++ b/src/test/java/dev/loki/lomines/util/ChunkUtilsRefreshTest.java @@ -16,7 +16,6 @@ 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.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,7 +37,7 @@ class ChunkUtilsRefreshTest { } @Test - void testUpdateChunks_withLargeRegion() { + 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); @@ -52,7 +51,7 @@ class ChunkUtilsRefreshTest { } @Test - void testUpdateChunks_withNoPlayers() { + 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); diff --git a/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java b/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java index 7deccd2..e62e85d 100644 --- a/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java +++ b/src/test/java/dev/loki/lomines/util/ChunkUtilsTest.java @@ -18,7 +18,6 @@ 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.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -43,14 +42,14 @@ class ChunkUtilsTest { } @Test - void testUpdateChunks_withNullRegion_throwsException() { + void testUpdateChunksWithNullRegionThrowsException() { assertThrows(IllegalArgumentException.class, () -> { ChunkUtils.updateChunks(null); }); } @Test - void testUpdateChunks_withSingleChunkRegion() { + void testUpdateChunksWithSingleChunkRegion() { // Create a small region within a single chunk Location loc1 = new Location(world, 0, 64, 0); Location loc2 = new Location(world, 5, 70, 5); @@ -69,7 +68,7 @@ class ChunkUtilsTest { } @Test - void testUpdateChunks_withMultipleChunks() { + void testUpdateChunksWithMultipleChunks() { // 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); @@ -88,7 +87,7 @@ class ChunkUtilsTest { } @Test - void testUpdateChunks_withNegativeCoordinates() { + void testUpdateChunksWithNegativeCoordinates() { // Test with negative coordinates Location loc1 = new Location(world, -16, 64, -16); Location loc2 = new Location(world, -1, 70, -1); diff --git a/src/test/java/dev/loki/lomines/util/LocationParserErrorTest.java b/src/test/java/dev/loki/lomines/util/LocationParserErrorTest.java index 3466d56..49ecf08 100644 --- a/src/test/java/dev/loki/lomines/util/LocationParserErrorTest.java +++ b/src/test/java/dev/loki/lomines/util/LocationParserErrorTest.java @@ -9,8 +9,6 @@ import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; class LocationParserErrorTest { diff --git a/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java b/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java index 2e2e04d..13f20fb 100644 --- a/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java +++ b/src/test/java/dev/loki/lomines/util/TimeFormatterTest.java @@ -4,8 +4,6 @@ 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 { diff --git a/src/test/java/dev/loki/lomines/util/selection/SelectionBoundsTest.java b/src/test/java/dev/loki/lomines/util/selection/SelectionBoundsTest.java index fd2387a..ebbf45f 100644 --- a/src/test/java/dev/loki/lomines/util/selection/SelectionBoundsTest.java +++ b/src/test/java/dev/loki/lomines/util/selection/SelectionBoundsTest.java @@ -9,7 +9,6 @@ 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.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; 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 1fae7e5..aa1e57e 100644 --- a/src/test/java/dev/loki/lomines/util/selection/SelectionManagerTest.java +++ b/src/test/java/dev/loki/lomines/util/selection/SelectionManagerTest.java @@ -26,7 +26,7 @@ class SelectionManagerTest { } @Test - void getSelection_createsNewSelectionIfNotExists() { + void getSelectionCreatesNewSelectionIfNotExists() { Selection selection = manager.getSelection(playerId); assertNotNull(selection); @@ -34,7 +34,7 @@ class SelectionManagerTest { } @Test - void getSelection_returnsSameInstanceOnMultipleCalls() { + void getSelectionReturnsSameInstanceOnMultipleCalls() { Selection first = manager.getSelection(playerId); Selection second = manager.getSelection(playerId); @@ -42,21 +42,21 @@ class SelectionManagerTest { } @Test - void getSelection_throwsExceptionForNullPlayerId() { + void getSelectionThrowsExceptionForNullPlayerId() { assertThrows(IllegalArgumentException.class, () -> { manager.getSelection(null); }); } @Test - void findSelection_returnsEmptyWhenNotExists() { + void findSelectionReturnsEmptyWhenNotExists() { Optional result = manager.findSelection(playerId); assertTrue(result.isEmpty()); } @Test - void findSelection_returnsSelectionWhenExists() { + void findSelectionReturnsSelectionWhenExists() { Selection created = manager.getSelection(playerId); Optional found = manager.findSelection(playerId); @@ -65,33 +65,33 @@ class SelectionManagerTest { } @Test - void findSelection_throwsExceptionForNullPlayerId() { + void findSelectionThrowsExceptionForNullPlayerId() { assertThrows(IllegalArgumentException.class, () -> { manager.findSelection(null); }); } @Test - void hasSelection_returnsFalseWhenNotExists() { + void hasSelectionReturnsFalseWhenNotExists() { assertFalse(manager.hasSelection(playerId)); } @Test - void hasSelection_returnsTrueWhenExists() { + void hasSelectionReturnsTrueWhenExists() { manager.getSelection(playerId); assertTrue(manager.hasSelection(playerId)); } @Test - void hasSelection_throwsExceptionForNullPlayerId() { + void hasSelectionThrowsExceptionForNullPlayerId() { assertThrows(IllegalArgumentException.class, () -> { manager.hasSelection(null); }); } @Test - void clearSelection_removesSelection() { + void clearSelectionRemovesSelection() { manager.getSelection(playerId); assertTrue(manager.hasSelection(playerId)); @@ -101,14 +101,14 @@ class SelectionManagerTest { } @Test - void clearSelection_doesNothingWhenSelectionNotExists() { + void clearSelectionDoesNothingWhenSelectionNotExists() { assertDoesNotThrow(() -> { manager.clearSelection(playerId); }); } @Test - void clearSelection_throwsExceptionForNullPlayerId() { + void clearSelectionThrowsExceptionForNullPlayerId() { 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 678b772..e5b12fd 100644 --- a/src/test/java/dev/loki/lomines/util/selection/SelectionTest.java +++ b/src/test/java/dev/loki/lomines/util/selection/SelectionTest.java @@ -1,17 +1,14 @@ 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 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/todo.md b/todo.md index ac429d3..27e527f 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,7 @@ # LoMines — План реструктуризации и улучшения качества -> **Обновлено:** 2026-07-06 -> **Цели:** Разбивка на подпапки (≤3 файла), лимит 150 строк/файл, Checkstyle +> **Обновлено:** 2026-08-12 +> **Цели:** Разбивка на подпапки (≤3 файла), лимит 150 строк/файл, Checkstyle, тестовое покрытие --- @@ -574,3 +574,28 @@ dev.loki.lomines/ - **0 файлов >150 строк** - **0 папок >3 файлов** - **145 main + 31 test = 176 файлов** + +--- + +## ✅ Постреструктуризационные задачи завершены (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`)