diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..961b063 --- /dev/null +++ b/.classpath @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..7624faf --- /dev/null +++ b/.project @@ -0,0 +1,34 @@ + + + LoMines + Project LoMines created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + + + 1783342209787 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..f7f3856 --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments=--init-script /home/loki/.cache/opencode/bin/jdtls/config_linux/org.eclipse.osgi/57/0/.cp/gradle/init/init.gradle +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/usr/lib/jvm/java-21-openjdk +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true 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/README.md b/README.md index dd1679d..f2719b7 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,141 @@ +
+ # LoMines -Advanced mine management plugin for Paper 1.21.4+ +Multi-region mine management plugin with WorldGuard integration and player statistics for Paper servers. -## Features +![Java](https://img.shields.io/badge/Java-21+-orange?style=flat-square&logo=openjdk&logoColor=white) +![Paper](https://img.shields.io/badge/Paper-1.19.2+-blue?style=flat-square) +![Folia](https://img.shields.io/badge/Folia-supported-purple?style=flat-square) +![License](https://img.shields.io/badge/license-GPLv3-blue?style=flat-square&logo=gnu&logoColor=white) +![version](https://img.shields.io/badge/version-3.0.0-green?style=flat-square) -- **Multiple fill modes**: Cuboid (fill entire region) or Mask (fill marked positions only) -- **Weighted block system**: Configure different blocks with spawn chances -- **Automatic resets**: Timer-based or percentage-based triggers -- **WorldGuard integration**: Auto-create regions for new mines -- **PlaceholderAPI support**: Display mine info in other plugins -- **GUI editor**: In-game mine configuration editor -- **Player statistics**: Track blocks mined, mines reset, playtime +[English](#english) | [Русский](#russian) -## Commands +
-### Admin Commands +--- + + + +## English + +### Overview + +LoMines is an advanced mine management plugin for Paper 1.19.2+ servers. It supports multiple fill modes, weighted block systems, automatic resets, WorldGuard integration, in-game GUI editor, and player statistics. + +### Features + +| Feature | Description | +|---------|-------------| +| Multiple fill modes | Cuboid (fill entire region) or Mask (fill marked positions) | +| Weighted blocks | Configure spawn chances per block type | +| Automatic resets | Timer-based or percentage-based triggers | +| WorldGuard integration | Auto-create regions for new mines | +| GUI editor | In-game mine configuration | +| Player statistics | Track blocks mined, resets, playtime | +| Leaderboards | Top players per mine or global | +| Group wand | Create multiple mines from batch selections | +| PlaceholderAPI | Display mine info in other plugins | +| Holograms | DecentHolograms and HolographicDisplays support | + +### Commands | Command | Permission | Description | |---------|------------|-------------| -| `/lm create ` | `lomines.admin.create` | Create a new mine | -| `/lm delete ` | `lomines.admin.delete` | Delete a mine | -| `/lm reset [silent]` | `lomines.admin.reset` | Reset a mine manually | -| `/lm reload` | `lomines.admin.reload` | Reload all mine configs | -| `/lm list` | `lomines.admin.list` | List all mines | -| `/lm edit ` | `lomines.admin.edit` | Open GUI editor for a mine | -| `/lm setteleport ` | `lomines.admin.setteleport` | Set teleport location (your current position) | -| `/lm setspawn ` | `lomines.admin.setspawn` | Set safe spawn location for stuck players | -| `/lm clearspawn ` | `lomines.admin.setspawn` | Clear spawn location | -| `/lm maskscan ` | `lomines.admin.maskscan` | Scan mask marker blocks | - -### Player Commands - -| Command | Permission | Description | -|---------|------------|-------------| -| `/lm stats [player]` | `lomines.stats` | View mining statistics | +| `/lm create ` | `lomines.admin` | Create a new mine | +| `/lm delete ` | `lomines.admin` | Delete a mine | +| `/lm reset [silent]` | `lomines.admin` | Reset a mine | +| `/lm reload` | `lomines.admin` | Reload configs | +| `/lm list` | `lomines.admin` | List all mines | +| `/lm edit ` | `lomines.admin.edit` | Open GUI editor | +| `/lm wand` | `lomines.admin.wand` | Get selection wand | +| `/lm group ` | `lomines.admin.wand` | Batch mine creation | +| `/lm setteleport ` | `lomines.admin.setteleport` | Set teleport point | +| `/lm setspawn ` | `lomines.admin.setspawn` | Set safe spawn | +| `/lm maskscan ` | `lomines.admin.maskscan` | Scan mask markers | +| `/lm stats [player]` | `lomines.stats` | View statistics | | `/lm top [mine] [limit]` | `lomines.stats` | View leaderboard | -### Wand Commands +### Placeholders -| Command | Permission | Description | -|---------|------------|-------------| -| `/lm wand` | `lomines.admin.wand` | Get the region selection wand | -| `/lm group ` | `lomines.admin.wand` | Create mines from group wand selections | +| Placeholder | Description | +|-------------|-------------| +| `%lomines_mine__blocks%` | Current blocks in mine | +| `%lomines_mine__percent%` | Fill percentage | +| `%lomines_mine__remaining%` | Remaining blocks | +| `%lomines_mine__resettime%` | Time since reset | +| `%lomines_player_blocksmined%` | Total blocks mined | +| `%lomines_player_rank%` | Leaderboard rank | +| `%lomines_count%` | Total mines count | -## Permissions +### Dependencies -```yaml -lomines.admin: - description: All admin commands - default: op - children: - lomines.use: true - lomines.admin.create: true - lomines.admin.delete: true - lomines.admin.reset: true - lomines.admin.reload: true - lomines.admin.list: true - lomines.admin.edit: true - lomines.admin.setteleport: true - lomines.admin.setspawn: true - lomines.admin.maskscan: true - lomines.admin.wand: true - lomines.stats: true - lomines.stats.others: true +- Required: Paper 1.19.2+, Java 21+ +- Optional: PlaceholderAPI, WorldGuard, DecentHolograms, HolographicDisplays, Oraxen, ItemsAdder -lomines.use: - description: Basic usage - default: true +### Installation -lomines.stats: - description: View own stats and leaderboards - default: true +1. Drop the jar into `plugins/` +2. Restart the server +3. Configure `plugins/LoMines/mines/` and `plugins/LoMines/defaults.yml` -lomines.stats.others: - description: View other players' stats - default: op -``` +--- -## Placeholders (PlaceholderAPI) + -### Mine Information +## Русский -| Placeholder | Description | Example | -|-------------|-------------|---------| -| `%lomines_mine__name%` | Mine name | `mymine` | -| `%lomines_mine__blocks%` | Current blocks | `8542` | -| `%lomines_mine__total%` | Total volume | `10000` | -| `%lomines_mine__percent%` | Fill percentage (1 decimal) | `85.4` | -| `%lomines_mine__percentint%` | Fill percentage (integer) | `85` | -| `%lomines_mine__world%` | World name | `world` | -| `%lomines_mine__remaining%` | Remaining blocks | `1458` | -| `%lomines_mine__resettime%` | Time since reset (mm:ss) | `03:45` | -| `%lomines_mine__resetseconds%` | Seconds since reset | `225` | +### Обзор -### Player Statistics +LoMines - продвинутый плагин управления шахтами для Paper 1.19.2+ серверов. Поддерживает несколько режимов заполнения, взвешенные блоки, автоматические сбросы, интеграцию с WorldGuard, GUI-редактор и статистику игроков. -| Placeholder | Description | Example | -|-------------|-------------|---------| -| `%lomines_player_blocksmined%` | Total blocks mined | `15234` | -| `%lomines_player_minesreset%` | Mines reset count | `45` | -| `%lomines_player_playtime%` | Formatted play time | `2h 15m` | -| `%lomines_player_rank%` | Leaderboard rank | `3` | +### Возможности -### Global +| Возможность | Описание | +|-------------|----------| +| Режимы заполнения | Cuboid (весь регион) или Mask (по меткам) | +| Взвешенные блоки | Настройка шансов появления для каждого блока | +| Авто-сбросы | По таймеру или по проценту добычи | +| WorldGuard | Авто-создание регионов для новых шахт | +| GUI-редактор | Настройка шахт прямо в игре | +| Статистика | Добыто блоков, сбросы, время игры | +| Лидерборды | Топ игроков по шахте или глобально | +| Групповая палка | Создание нескольких шахт за раз | +| PlaceholderAPI | Информация о шахтах в других плагинах | +| Голограммы | Поддержка DecentHolograms и HolographicDisplays | -| Placeholder | Description | Example | -|-------------|-------------|---------| -| `%lomines_count%` | Total number of mines | `12` | +### Команды -## Configuration +| Команда | Право | Описание | +|---------|-------|----------| +| `/lm create <имя>` | `lomines.admin` | Создать шахту | +| `/lm delete <имя>` | `lomines.admin` | Удалить шахту | +| `/lm reset <имя>` | `lomines.admin` | Сбросить шахту | +| `/lm edit <имя>` | `lomines.admin.edit` | Открыть GUI-редактор | +| `/lm wand` | `lomines.admin.wand` | Получить палку выделения | +| `/lm stats [игрок]` | `lomines.stats` | Статистика | +| `/lm top [шахта]` | `lomines.stats` | Лидерборд | -### defaults.yml +### Зависимости -Default configuration applied to all mines: +- Обязательные: Paper 1.19.2+, Java 21+ +- Опциональные: PlaceholderAPI, WorldGuard, DecentHolograms, Oraxen, ItemsAdder -```yaml -# Reset settings -reset: - interval: "5m" # Reset interval (30s, 5m, 2h, 1d) - percent-trigger: 10.0 # Trigger reset when X% of blocks mined - percent-enabled: false # Enable percentage trigger - commands: [] # Commands to run on reset - broadcast: "" # Broadcast message on reset +### Установка -# Block settings -blocks: - fill-mode: CUBOID # CUBOID or MASK - mask: - marker: "pink_concrete" # Marker block for mask mode +1. Положите jar в папку `plugins/` +2. Перезапустите сервер +3. Настройте `plugins/LoMines/mines/` и `plugins/LoMines/defaults.yml` -# Teleport settings -teleport: - enabled: false - location: "" +--- -# Safe spawn for stuck players -player-spawn: - enabled: false - location: "" +### Links -# UI settings -ui: - actionbar: - enabled: true - format: "{mine} {percent}% ({time})" - range: 50.0 +- [Releases](../../releases) +- [Issues](../../issues) +- [License](LICENSE) -# WorldGuard integration -worldguard: - enabled: false - region-template: "{mine_name}_{random_4}" - owners: [] - members: [] - flags: - - "passthrough=deny" - - "build=allow" -``` +### License -## Usage Examples - -### Creating a Simple Mine - -```bash -# 1. Get the wand -/lm wand - -# 2. Select two corners (left and right click) -# 3. Create the mine -/lm create mymine - -# 4. Edit the config file or use GUI -/lm edit mymine -``` - -### Setting Up Teleport - -```bash -# Stand where players should be teleported -/lm setteleport mymine - -# Stand where STUCK players should spawn (optional, falls back to teleport location) -/lm setspawn mymine -``` - -### Using Mask Fill Mode - -```bash -# 1. Paint positions with pink_concrete (or configured marker) -# 2. Scan the markers -/lm maskscan mymine - -# Mine will now only fill at marked positions -``` - -### WorldGuard Auto-Regions - -Enable in `plugins/LoMines/mines/_defaults.yml`: - -```yaml -worldguard: - enabled: true - region-template: "mine_{mine_name}_{random_4}" - flags: - - "passthrough=deny" - - "build=allow" -``` - -When you create a mine, a WorldGuard region `mine_mymine_7392` is automatically created. - -## Support - -- Paper 1.21.4+ -- Java 21+ -- Optional: PlaceholderAPI, WorldGuard - -## License - -MIT License +GNU General Public License v3.0 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 0caa85a..1dd9a63 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { java id("com.github.johnrengelman.shadow") version "8.1.1" + checkstyle } group = "com.loki" @@ -13,79 +14,88 @@ java { } } +tasks.withType { + options.release.set(21) +} + repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") + maven("https://maven.enginehub.org/repo/") + maven("https://repo.codemc.io/repository/maven-public/") maven("https://jitpack.io") } -// LoAPI: libs/lolib*.jar — fileTree индексируется Gradle / Java LS стабильнее, чем files(singleJar) -val lolibTree = fileTree("libs") { include("lolib*.jar") } -if (lolibTree.files.isEmpty()) { - throw GradleException( - "Missing libs/lolib*.jar — add LoAPI fat jar to ${rootProject.file("libs").absolutePath}" - ) -} - dependencies { - // Paper API 1.21.4 (includes Adventure API) compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + implementation("org.apache.commons:commons-math3:3.6.1") + implementation("commons-io:commons-io:2.18.0") + compileOnly("me.clip:placeholderapi:2.11.6") + compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.13") + compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10") - // LoAPI (local) - implementation(lolibTree) - - // Runtime dependencies (loaded via DependencyManager from LoAPI) - // These are marked as compileOnly because they will be downloaded at runtime - compileOnly("org.apache.commons:commons-math3:3.6.1") - compileOnly("commons-io:commons-io:2.18.0") - - // Adventure API (included in Paper, but explicit for clarity) - compileOnly("net.kyori:adventure-api:4.17.0") - compileOnly("net.kyori:adventure-text-minimessage:4.17.0") - - // Optional plugin integrations (soft dependencies) - // compileOnly("com.github.oraxen:oraxen:1.180.0") - // compileOnly("com.github.LoneDev6:api-itemsadder:3.6.4") - - // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") testImplementation("io.kotest:kotest-runner-junit5:5.9.1") testImplementation("io.kotest:kotest-assertions-core:5.9.1") testImplementation("io.kotest:kotest-property:5.9.1") testImplementation("org.mockito:mockito-core:5.14.2") - testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") - testImplementation("org.apache.commons:commons-math3:3.6.1") - testImplementation("commons-io:commons-io:2.18.0") + testImplementation("org.mockito:mockito-inline:5.2.0") + testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.110.0") { + exclude(group = "org.junit.jupiter", module = "junit-jupiter-api") + } + testImplementation("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + testImplementation("net.kyori:adventure-api:4.17.0") + testImplementation("net.kyori:adventure-text-minimessage:4.17.0") + testImplementation("com.sk89q.worldguard:worldguard-bukkit:7.0.13") +} + +checkstyle { + toolVersion = "10.21.4" + configFile = file("config/checkstyle/checkstyle.xml") + isIgnoreFailures = false + maxErrors = 0 + maxWarnings = 0 +} + +tasks.withType().configureEach { + reports { + xml.required.set(true) + html.required.set(true) + } } tasks { + shadowJar { archiveClassifier.set("") - - // Relocate LoAPI to avoid conflicts - relocate("dev.loki.lomines.libs.lolib", "com.loki.lomines.libs.lolib") - - // Don't minimize - causes issues with Java 21 - // minimize() + + // Temporary: disable relocate to avoid ASM Java 21 bytecode issues + // relocate( + // "dev.loki.lomines.libs.lolib", + // "com.loki.lomines.libs.lolib" + // ) } - + build { dependsOn(shadowJar) } - + test { useJUnitPlatform() } - + processResources { val props = mapOf( "version" to version, "description" to description ) + inputs.properties(props) filteringCharset = "UTF-8" + filesMatching("plugin.yml") { expand(props) } } -} +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..946b028 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 63f2184..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index c8f7d56..db3a6ac 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,7 +1,94 @@ -@rem Gradle startup script for Windows +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + @if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -java -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libs/.gitkeep b/libs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/dev/loki/lomines/ComponentInitializer.java b/src/main/java/dev/loki/lomines/ComponentInitializer.java index eb5952b..42a9dc7 100644 --- a/src/main/java/dev/loki/lomines/ComponentInitializer.java +++ b/src/main/java/dev/loki/lomines/ComponentInitializer.java @@ -1,11 +1,11 @@ package dev.loki.lomines; -import dev.loki.lomines.core.mine.MineTicker; -import dev.loki.lomines.core.mine.Mines; -import dev.loki.lomines.data.stats.StatsManager; +import dev.loki.lomines.core.mine.service.MineTicker; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.data.stats.service.StatsManager; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.wand.group.GroupWandManager; -import dev.lolib.commands.CommandManager; + import java.io.IOException; import java.nio.file.Files; @@ -28,7 +28,7 @@ final class ComponentInitializer { if (!Files.exists(minesFolder)) { Files.createDirectories(minesFolder); - plugin.loLogger().info("Created mines directory"); + plugin.getLogger().info("Created mines directory"); } } @@ -36,20 +36,19 @@ final class ComponentInitializer { Mines mines = new Mines(plugin); GroupWandManager groupWandManager = new GroupWandManager(); StatsManager statsManager = new StatsManager(plugin); - CommandManager commandManager = new CommandManager(plugin); IntegrationManager integrationManager = new IntegrationManager(plugin); - plugin.loLogger().info("Core components initialized"); + plugin.getLogger().info("Core components initialized"); - return new Components(mines, groupWandManager, statsManager, commandManager, integrationManager); + return new Components(mines, groupWandManager, statsManager, integrationManager); } void loadMines(Mines mines) { try { mines.loadAll(); - plugin.loLogger().info("Loaded " + mines.getAll().size() + " mine(s)"); + plugin.getLogger().info("Loaded " + mines.getAll().size() + " mine(s)"); } catch (IOException e) { - plugin.loLogger().error("Failed to load mines: " + e.getMessage()); + plugin.getLogger().severe("Failed to load mines: " + e.getMessage()); e.printStackTrace(); } } @@ -65,9 +64,9 @@ final class ComponentInitializer { try { statsManager.load(); statsManager.startAutoSave(); - plugin.loLogger().info("Statistics system enabled"); + plugin.getLogger().info("Statistics system enabled"); } catch (IOException e) { - plugin.loLogger().warn("Failed to load statistics: " + e.getMessage()); + plugin.getLogger().warning("Failed to load statistics: " + e.getMessage()); } } } @@ -76,7 +75,6 @@ final class ComponentInitializer { Mines mines, GroupWandManager groupWandManager, StatsManager statsManager, - CommandManager commandManager, IntegrationManager integrationManager ) { } diff --git a/src/main/java/dev/loki/lomines/LoMinesPlugin.java b/src/main/java/dev/loki/lomines/LoMinesPlugin.java index b80a7ec..e6d46f9 100644 --- a/src/main/java/dev/loki/lomines/LoMinesPlugin.java +++ b/src/main/java/dev/loki/lomines/LoMinesPlugin.java @@ -1,33 +1,26 @@ package dev.loki.lomines; -import dev.loki.lomines.core.mine.MineTicker; -import dev.loki.lomines.core.mine.Mines; -import dev.loki.lomines.data.stats.StatsManager; +import dev.loki.lomines.core.mine.service.MineTicker; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.data.stats.service.StatsManager; import dev.loki.lomines.integration.IntegrationManager; import dev.loki.lomines.integration.hologram.HologramManager; import dev.loki.lomines.wand.WandParticleService; import dev.loki.lomines.wand.group.GroupWandManager; -import dev.lolib.commands.CommandManager; -import dev.lolib.core.LoPlugin; -import dev.lolib.core.dependency.DependencyManager; +import org.bukkit.plugin.java.JavaPlugin; -/** - * Main plugin class for LoMines. - * Extends LoPlugin from LoAPI for Paper/Folia compatibility. - */ -public final class LoMinesPlugin extends LoPlugin { +public final class LoMinesPlugin extends JavaPlugin { private Mines mines; private MineTicker mineTicker; private GroupWandManager groupWandManager; private StatsManager statsManager; - private CommandManager commandManager; private IntegrationManager integrationManager; private WandParticleService wandParticleService; private HologramManager hologramManager; @Override - protected void enable() { + public void onEnable() { try { saveDefaultConfig(); @@ -38,7 +31,6 @@ public final class LoMinesPlugin extends LoPlugin { this.mines = components.mines(); this.groupWandManager = components.groupWandManager(); this.statsManager = components.statsManager(); - this.commandManager = components.commandManager(); this.integrationManager = components.integrationManager(); this.wandParticleService = new WandParticleService(this); this.hologramManager = new HologramManager(this); @@ -47,65 +39,51 @@ public final class LoMinesPlugin extends LoPlugin { this.mineTicker = initializer.startTicker(mines); RegistrationManager registrationManager = new RegistrationManager(this); - registrationManager.registerCommands(commandManager); - registrationManager.registerTabCompleter(); + registrationManager.registerCommands(); registrationManager.registerListeners(); registrationManager.initializeIntegrations(integrationManager); initializer.startStatistics(statsManager); - loLogger().info("LoMines has been enabled!"); + getLogger().info("LoMines has been enabled!"); } catch (Exception e) { - loLogger().error("Failed to enable LoMines: " + e.getMessage()); + getLogger().severe("Failed to enable LoMines: " + e.getMessage()); e.printStackTrace(); getServer().getPluginManager().disablePlugin(this); } } @Override - protected void disable() { + public void onDisable() { try { if (mineTicker != null) { mineTicker.stop(); mineTicker = null; } - if (mines != null) { mines.getAll().forEach(mine -> mine.stop()); } - if (statsManager != null) { statsManager.stopAutoSave(); statsManager.save(); - loLogger().info("Statistics saved successfully"); + getLogger().info("Statistics saved successfully"); } - if (integrationManager != null) { integrationManager.shutdown(); } - if (wandParticleService != null) { wandParticleService.stopAll(); } - if (hologramManager != null) { hologramManager.shutdown(); } - - loLogger().info("LoMines has been disabled!"); + getLogger().info("LoMines has been disabled!"); } catch (Exception e) { - loLogger().error("Error during plugin shutdown: " + e.getMessage()); + getLogger().severe("Error during plugin shutdown: " + e.getMessage()); e.printStackTrace(); } } - @Override - @SuppressWarnings("removal") - protected void dependencies(DependencyManager dependencyManager) { - dependencyManager.add("org.apache.commons", "commons-math3", "3.6.1"); - dependencyManager.add("commons-io", "commons-io", "2.18.0"); - } - public Mines getMines() { return mines; } @@ -118,10 +96,6 @@ public final class LoMinesPlugin extends LoPlugin { return statsManager; } - public CommandManager getCommandManager() { - return commandManager; - } - public IntegrationManager getIntegrationManager() { return integrationManager; } diff --git a/src/main/java/dev/loki/lomines/RegistrationManager.java b/src/main/java/dev/loki/lomines/RegistrationManager.java index d8e7436..32b878e 100644 --- a/src/main/java/dev/loki/lomines/RegistrationManager.java +++ b/src/main/java/dev/loki/lomines/RegistrationManager.java @@ -1,22 +1,13 @@ package dev.loki.lomines; -import dev.loki.lomines.command.admin.AdminCommands; -import dev.loki.lomines.command.admin.HologramCommands; -import dev.loki.lomines.command.admin.MaskCommands; -import dev.loki.lomines.command.admin.StatsCommands; -import dev.loki.lomines.command.common.LoMinesTabCompleter; -import dev.loki.lomines.command.player.PlayerCommands; +import dev.loki.lomines.command.LmCommand; import dev.loki.lomines.integration.IntegrationManager; -import dev.loki.lomines.listener.BlockBreakListener; -import dev.loki.lomines.listener.GroupGuiListener; -import dev.loki.lomines.listener.MineEditGuiListener; -import dev.loki.lomines.listener.PlayerInteractListener; -import dev.loki.lomines.listener.PlayerJoinListener; -import dev.lolib.commands.CommandManager; +import dev.loki.lomines.listener.block.BlockBreakListener; +import dev.loki.lomines.listener.gui.GroupGuiListener; +import dev.loki.lomines.listener.gui.MineEditGuiListener; +import dev.loki.lomines.listener.player.PlayerInteractListener; +import dev.loki.lomines.listener.player.PlayerJoinListener; -/** - * Handles registration of commands, listeners, and integrations. - */ final class RegistrationManager { private final LoMinesPlugin plugin; @@ -25,24 +16,16 @@ final class RegistrationManager { this.plugin = plugin; } - void registerCommands(CommandManager commandManager) { - commandManager.register(new AdminCommands(plugin)); - commandManager.register(new PlayerCommands(plugin)); - commandManager.register(new StatsCommands(plugin)); - commandManager.register(new MaskCommands(plugin)); - commandManager.register(new HologramCommands(plugin)); - - plugin.loLogger().info("Commands registered"); - } - - void registerTabCompleter() { - LoMinesTabCompleter tabCompleter = new LoMinesTabCompleter(plugin); - plugin.getCommand("lm").setTabCompleter(tabCompleter); - plugin.getCommand("lomines").setTabCompleter(tabCompleter); - plugin.getCommand("mine").setTabCompleter(tabCompleter); - plugin.getCommand("mines").setTabCompleter(tabCompleter); - - plugin.loLogger().info("Tab completer registered"); + void registerCommands() { + LmCommand executor = new LmCommand(plugin); + for (String alias : new String[]{"lm", "lomines", "mine", "mines"}) { + var cmd = plugin.getCommand(alias); + if (cmd != null) { + cmd.setExecutor(executor); + cmd.setTabCompleter(executor); + } + } + plugin.getLogger().info("Commands registered"); } void registerListeners() { @@ -54,12 +37,11 @@ final class RegistrationManager { if (plugin.getConfig().getBoolean("statistics-enabled", true)) { plugin.getServer().getPluginManager().registerEvents(new PlayerJoinListener(plugin), plugin); } - - plugin.loLogger().info("Event listeners registered"); + plugin.getLogger().info("Event listeners registered"); } void initializeIntegrations(IntegrationManager integrationManager) { integrationManager.initAll(); - plugin.loLogger().info("Integrations initialized"); + plugin.getLogger().info("Integrations initialized"); } } diff --git a/src/main/java/dev/loki/lomines/block/BlockSetter.java b/src/main/java/dev/loki/lomines/block/BlockSetter.java index 537ac5a..be5fcb0 100644 --- a/src/main/java/dev/loki/lomines/block/BlockSetter.java +++ b/src/main/java/dev/loki/lomines/block/BlockSetter.java @@ -1,6 +1,6 @@ package dev.loki.lomines.block; -import dev.loki.lomines.util.location.Cuboid; +import dev.loki.lomines.util.location.geo.Cuboid; import org.bukkit.Location; import java.util.List; diff --git a/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java b/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java index e336bcd..2c93c22 100644 --- a/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java +++ b/src/main/java/dev/loki/lomines/block/BukkitBlockSetter.java @@ -3,8 +3,8 @@ package dev.loki.lomines.block; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.util.block.BlockUpdateUtil; -import dev.loki.lomines.util.location.Cuboid; -import dev.lolib.scheduler.Scheduler; +import dev.loki.lomines.util.location.geo.Cuboid; + import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -35,11 +35,11 @@ public final class BukkitBlockSetter extends BlockSetter { @Override public void fill(Cuboid region, IntConsumer callback) { - Scheduler.get(plugin).runAsync(() -> { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int count = fillSync(region); // Send block updates on main thread to prevent ghost blocks - Scheduler.get(plugin).run(() -> { + Bukkit.getScheduler().runTask(plugin, () -> { BlockUpdateUtil.sendRegionUpdate( region.getWorld(), region.getMinX(), region.getMinY(), region.getMinZ(), @@ -53,14 +53,14 @@ public final class BukkitBlockSetter extends BlockSetter { @Override public void fillAtLocations(List locations, IntConsumer callback) { if (locations == null || locations.isEmpty()) { - Scheduler.get(plugin).run(() -> callback.accept(0)); + Bukkit.getScheduler().runTask(plugin, () -> callback.accept(0)); return; } - Scheduler.get(plugin).runAsync(() -> { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int count = fillAtLocationsSync(locations); // Send block updates on main thread to prevent ghost blocks - Scheduler.get(plugin).run(() -> { + Bukkit.getScheduler().runTask(plugin, () -> { BlockUpdateUtil.sendLocationsUpdate(locations); callback.accept(count); }); @@ -106,8 +106,8 @@ public final class BukkitBlockSetter extends BlockSetter { currentWeight += entry.getValue(); if (random <= currentWeight) { // BlockKey is already validated to be a block - if (entry.getKey() instanceof BlockKey.Vanilla vanilla) { - return Bukkit.createBlockData(vanilla.material()); + if (entry.getKey() instanceof BlockKey.Vanilla(Material material)) { + return Bukkit.createBlockData(material); } // Fallback for non-vanilla keys in vanilla setter (shouldn't happen) return Bukkit.createBlockData(Material.STONE); @@ -116,8 +116,8 @@ public final class BukkitBlockSetter extends BlockSetter { // Fallback to first entry BlockKey firstKey = weights.keySet().iterator().next(); - if (firstKey instanceof BlockKey.Vanilla vanilla) { - return Bukkit.createBlockData(vanilla.material()); + if (firstKey instanceof BlockKey.Vanilla(Material material)) { + return Bukkit.createBlockData(material); } return Bukkit.createBlockData(Material.STONE); } diff --git a/src/main/java/dev/loki/lomines/command/LmCommand.java b/src/main/java/dev/loki/lomines/command/LmCommand.java new file mode 100644 index 0000000..edcde6c --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/LmCommand.java @@ -0,0 +1,85 @@ +package dev.loki.lomines.command; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.command.admin.info.InfoCommand; +import dev.loki.lomines.command.admin.info.RegionCommands; +import dev.loki.lomines.command.admin.manage.AdminCommands; +import dev.loki.lomines.command.admin.manage.CopyCommand; +import dev.loki.lomines.command.admin.misc.MaskCommands; +import dev.loki.lomines.command.admin.misc.TeleportCommands; +import dev.loki.lomines.command.admin.stats.HologramCommands; +import dev.loki.lomines.command.admin.stats.StatsCommands; +import dev.loki.lomines.command.player.PlayerCommands; +import dev.loki.lomines.command.player.TeleportCommand; +import dev.loki.lomines.command.common.LoMinesTabCompleter; +import dev.loki.lomines.util.ErrorHandler; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class LmCommand implements CommandExecutor, TabCompleter { + + private final LoMinesPlugin plugin; + private final AdminCommands adminCommands; + private final InfoCommand infoCommand; + private final RegionCommands regionCommands; + private final CopyCommand copyCommand; + private final MaskCommands maskCommands; + private final TeleportCommands teleportCommands; + private final HologramCommands hologramCommands; + private final StatsCommands statsCommands; + private final PlayerCommands playerCommands; + private final TeleportCommand tpCommand; + private final LoMinesTabCompleter tabCompleter; + + public LmCommand(LoMinesPlugin plugin) { + this.plugin = plugin; + this.adminCommands = new AdminCommands(plugin); + this.infoCommand = new InfoCommand(plugin); + this.regionCommands = new RegionCommands(plugin); + this.copyCommand = new CopyCommand(plugin); + this.maskCommands = new MaskCommands(plugin); + this.teleportCommands = new TeleportCommands(plugin, plugin.getMines(), new ErrorHandler(plugin.getLogger(), "[LoMines] ")); + this.hologramCommands = new HologramCommands(plugin); + this.statsCommands = new StatsCommands(plugin); + this.playerCommands = new PlayerCommands(plugin); + this.tpCommand = new TeleportCommand(plugin); + this.tabCompleter = new LoMinesTabCompleter(plugin); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + sender.sendMessage("§6/lm help §f- Show help"); + return true; + } + + String sub = args[0].toLowerCase(); + String[] rest = new String[args.length - 1]; + System.arraycopy(args, 1, rest, 0, rest.length); + + switch (sub) { + case "create", "delete", "edit", "reset", "reload", "list" -> adminCommands.handle(sender, sub, rest); + case "info" -> infoCommand.handle(sender, rest); + case "regions", "addregion", "removeregion" -> regionCommands.handle(sender, sub, rest); + case "copy" -> copyCommand.handle(sender, rest); + case "maskscan" -> maskCommands.handle(sender, rest); + case "setteleport", "setspawn", "clearspawn" -> teleportCommands.handle(sender, sub, rest); + case "hologram" -> hologramCommands.handle(sender, rest); + case "stats", "top" -> statsCommands.handle(sender, sub, rest); + case "tp" -> tpCommand.handle(sender, rest); + case "wand", "group", "help" -> playerCommands.handle(sender, sub, rest); + default -> sender.sendMessage("§cUnknown command: " + sub); + } + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { + return tabCompleter.onTabComplete(sender, command, alias, args); + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java b/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java deleted file mode 100644 index d6d293a..0000000 --- a/src/main/java/dev/loki/lomines/command/admin/StatsCommands.java +++ /dev/null @@ -1,186 +0,0 @@ -package dev.loki.lomines.command.admin; - -import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.Mine; -import dev.loki.lomines.core.mine.Mines; -import dev.loki.lomines.data.stats.Leaderboard; -import dev.loki.lomines.data.stats.LeaderboardEntry; -import dev.loki.lomines.data.stats.PlayerStats; -import dev.loki.lomines.data.stats.StatsManager; -import dev.loki.lomines.util.ErrorHandler; -import dev.lolib.commands.annotation.Arg; -import dev.lolib.commands.annotation.Command; -import dev.lolib.commands.annotation.Subcommand; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** - * Statistics commands for player mining stats and leaderboards. - * Provides commands for viewing stats and top players. - */ -@Command(value = "lm", permission = "lomines.use") -public class StatsCommands { - - private final LoMinesPlugin plugin; - private final Mines mines; - private final ErrorHandler errorHandler; - - public StatsCommands(LoMinesPlugin plugin) { - this.plugin = plugin; - this.mines = plugin.getMines(); - this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); - } - - private static void sendTopRows(CommandSender sender, List rows) { - if (rows.isEmpty()) { - sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY)); - return; - } - for (int i = 0; i < rows.size(); i++) { - LeaderboardEntry e = rows.get(i); - String name = formatPlayerName(e.playerId()); - sender.sendMessage(Component.text( - (i + 1) + ". " + name + " — " + e.count(), - NamedTextColor.WHITE)); - } - } - - private static String formatPlayerName(UUID id) { - String name = Bukkit.getOfflinePlayer(id).getName(); - return name != null ? name : id.toString(); - } - - private static int clampLimit(int limit) { - return Math.max(1, Math.min(50, limit)); - } - - private static Integer tryParsePositiveInt(String s) { - try { - int v = Integer.parseInt(s); - return v > 0 ? v : null; - } catch (NumberFormatException e) { - return null; - } - } - - /** - * Shows mining stats for self or another player. - * Usage: /lm stats [player] - */ - @Subcommand(value = "stats", permission = "lomines.stats") - public void stats(CommandSender sender, @Arg(value = "player", optional = true, defaultValue = "") String targetName) { - if (isStatsDisabled(sender)) { - return; - } - String name = targetName == null ? "" : targetName.trim(); - UUID uuid; - if (name.isEmpty()) { - if (!(sender instanceof Player self)) { - errorHandler.sendError(sender, "Укажите игрока: /lm stats <ник>"); - return; - } - uuid = self.getUniqueId(); - } else { - if (!sender.hasPermission("lomines.stats.others")) { - errorHandler.handlePermissionDenied(sender, "смотреть чужую статистику"); - return; - } - Player online = Bukkit.getPlayerExact(name); - if (online != null) { - uuid = online.getUniqueId(); - } else { - OfflinePlayer off = Bukkit.getOfflinePlayer(name); - uuid = off.getUniqueId(); - } - } - StatsManager sm = plugin.getStatsManager(); - PlayerStats ps = sm.getOrCreate(uuid); - String displayName = formatPlayerName(uuid); - sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD)); - sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE)); - int pos = sm.getLeaderboard().getPosition(uuid); - if (pos > 0) { - sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY)); - } - Map byMine = ps.getMineStatsSnapshot(); - if (byMine.isEmpty()) { - return; - } - sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY)); - byMine.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .limit(12) - .forEach(e -> sender.sendMessage(Component.text( - " " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA))); - } - - /** - * Leaderboard: global or per-mine. - * Usage: /lm top — глобально 10; /lm top 25 — глобально 25; /lm top <шахта> — по шахте; - * /lm top <шахта> <лимит> - */ - @Subcommand(value = "top", permission = "lomines.stats") - public void top( - CommandSender sender, - @Arg(value = "arg1", optional = true, defaultValue = "") String arg1, - @Arg(value = "arg2", optional = true, defaultValue = "") String arg2 - ) { - if (isStatsDisabled(sender)) { - return; - } - String a = arg1 == null ? "" : arg1.trim(); - String b = arg2 == null ? "" : arg2.trim(); - int limit; - String mineName; - if (a.isEmpty()) { - limit = clampLimit(10); - mineName = null; - } else if (b.isEmpty()) { - Integer asLimit = tryParsePositiveInt(a); - if (asLimit != null) { - limit = clampLimit(asLimit); - mineName = null; - } else { - limit = clampLimit(10); - mineName = a; - } - } else { - mineName = a; - Integer limParsed = tryParsePositiveInt(b); - limit = clampLimit(limParsed != null ? limParsed : 10); - } - Leaderboard lb = plugin.getStatsManager().getLeaderboard(); - if (mineName != null) { - var found = mines.find(mineName); - if (found.isEmpty()) { - errorHandler.handleNotFound(sender, "Шахта", mineName); - return; - } - Mine m = found.get(); - String key = m.getName(); - List rows = lb.getTopByMine(key, limit); - sender.sendMessage(Component.text("=== Топ по шахте «" + key + "» ===", NamedTextColor.GOLD)); - sendTopRows(sender, rows); - return; - } - List rows = lb.getTopTotal(limit); - sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD)); - sendTopRows(sender, rows); - } - - private boolean isStatsDisabled(CommandSender sender) { - if (!plugin.getConfig().getBoolean("statistics-enabled", true)) { - sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED)); - return true; - } - return false; - } -} diff --git a/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java b/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java new file mode 100644 index 0000000..2c82737 --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/info/InfoCommand.java @@ -0,0 +1,96 @@ +package dev.loki.lomines.command.admin.info; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.data.config.block.BlockKey; +import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.util.location.geo.Cuboid; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; + +import java.util.List; +import java.util.Map; + +public class InfoCommand { + + private final LoMinesPlugin plugin; + + public InfoCommand(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + public void handle(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm info ", NamedTextColor.RED)); + return; + } + String mineName = args[0]; + + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + + var config = mine.getConfig(); + sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); + sender.sendMessage(Component.text("§6§lИнформация о шахте: §f" + mine.getName())); + sender.sendMessage(Component.text("")); + sender.sendMessage(Component.text("§7Мир: §f" + config.worldName())); + sender.sendMessage(Component.text("§7Блоков: §f" + mine.getBlocks() + "§7/§f" + mine.getTotalVolume() + + " §7(§f" + String.format("%.1f", mine.getPercentFilled()) + "%§7)")); + + printRegions(sender, mine.getRegions()); + printBlocks(sender, config.blocks().weights()); + printResetAndTeleport(sender, config); + + sender.sendMessage(Component.text("§6§lНаграды: §f" + config.rewards().entries().size() + " записей")); + sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); + } + + private void printRegions(CommandSender sender, List regions) { + sender.sendMessage(Component.text("§6§lРегионы:")); + for (int i = 0; i < regions.size(); i++) { + Cuboid r = regions.get(i); + sender.sendMessage(Component.text("§7 #" + (i + 1) + ": §f" + + r.getMinX() + "," + r.getMinY() + "," + r.getMinZ() + " §7-> §f" + + r.getMaxX() + "," + r.getMaxY() + "," + r.getMaxZ() + + " §7(§f" + r.getVolume() + " §7блоков)")); + } + sender.sendMessage(Component.text("")); + } + + private void printBlocks(CommandSender sender, Map weights) { + sender.sendMessage(Component.text("§6§lБлоки:")); + weights.entrySet().stream() + .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) + .limit(10) + .forEach(e -> sender.sendMessage(Component.text( + "§7 " + formatBlockKey(e.getKey()) + ": §f" + String.format("%.1f", e.getValue() * 100) + "%"))); + if (weights.size() > 10) { + sender.sendMessage(Component.text("§7 ... и ещё " + (weights.size() - 10) + " блоков")); + } + } + + private void printResetAndTeleport(CommandSender sender, MineConfig config) { + sender.sendMessage(Component.text("§6§lНастройки сброса:")); + sender.sendMessage(Component.text("§7 Интервал: §f" + config.reset().intervalDisplay())); + sender.sendMessage(Component.text("§7 Триггер по %: §f" + + (config.reset().isPercentTriggerEnabled() ? config.reset().percentTrigger() + "%" : "выкл"))); + + sender.sendMessage(Component.text("§6§lТелепортация:")); + sender.sendMessage(Component.text("§7 Точка телепорта: §f" + + (config.teleport().enabled() ? "§aустановлена" : "§7не задана"))); + sender.sendMessage(Component.text("§7 Точка спавна: §f" + + (config.playerSpawn().enabled() ? "§aустановлена" : "§7используется точка телепорта"))); + } + + private String formatBlockKey(BlockKey key) { + return switch (key) { + case BlockKey.Vanilla v -> v.material().name().toLowerCase(); + case BlockKey.Oraxen o -> "oraxen:" + o.id(); + case BlockKey.ItemsAdder i -> "ia:" + i.id(); + }; + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java b/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java new file mode 100644 index 0000000..db92fe2 --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/info/RegionActionHandler.java @@ -0,0 +1,127 @@ +package dev.loki.lomines.command.admin.info; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.data.config.region.RegionConfig; +import dev.loki.lomines.util.location.geo.Cuboid; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +public final class RegionActionHandler { + + private final LoMinesPlugin plugin; + + public RegionActionHandler(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + public void addRegion(CommandSender sender, String mineName) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED)); + return; + } + + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + + Cuboid newRegion = resolveNewRegion(player, mine); + if (newRegion == null) { + return; + } + + List currentRegions = new ArrayList<>(mine.getRegions()); + currentRegions.add(newRegion); + + updateMineRegions(mine, new RegionConfig(currentRegions)); + + sendRegionAdded(player, newRegion, currentRegions.size()); + } + + private Cuboid resolveNewRegion(Player player, Mine mine) { + var wandManager = plugin.getGroupWandManager(); + var session = wandManager.getSession(player.getUniqueId()); + int slot = session.getActiveSlot(); + var pos1 = session.getPos1(slot); + var pos2 = session.getPos2(slot); + if (pos1 == null || pos2 == null) { + player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка")); + return null; + } + Cuboid newRegion = new Cuboid(pos1, pos2); + String mineWorld = mine.getConfig().region().worldName(); + String regionWorld = newRegion.getWorld().getName(); + + if (!mineWorld.equals(regionWorld)) { + player.sendMessage(Component.text("§cРегион должен быть в мире §f" + mineWorld + "§c, а не §f" + regionWorld)); + return null; + } + session.clearCorners(slot); + return newRegion; + } + + private void sendRegionAdded(Player player, Cuboid newRegion, int count) { + player.sendMessage(Component.text("§aДобавлен регион #" + count)); + player.sendMessage(Component.text("§7От: §f" + newRegion.getMinX() + ", " + newRegion.getMinY() + ", " + newRegion.getMinZ())); + player.sendMessage(Component.text("§7До: §f" + newRegion.getMaxX() + ", " + newRegion.getMaxY() + ", " + newRegion.getMaxZ())); + player.sendMessage(Component.text("§7Объём: §f" + newRegion.getVolume() + " §7блоков")); + } + + public void removeRegion(CommandSender sender, String mineName, int index) { + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + + List currentRegions = new ArrayList<>(mine.getRegions()); + + if (currentRegions.size() <= 1) { + sender.sendMessage(Component.text("§cНельзя удалить последний регион! Удалите всю шахту.")); + return; + } + + if (index < 1 || index > currentRegions.size()) { + sender.sendMessage(Component.text("§cНеверный номер региона. Доступны: 1-" + currentRegions.size())); + return; + } + + Cuboid removed = currentRegions.remove(index - 1); + var newRegionConfig = new RegionConfig(currentRegions); + updateMineRegions(mine, newRegionConfig); + + sender.sendMessage(Component.text("§aУдалён регион #" + index)); + sender.sendMessage(Component.text("§7Был: §f" + removed.getMinX() + ", " + removed.getMinY() + ", " + removed.getMinZ() + + " §7-> §f" + removed.getMaxX() + ", " + removed.getMaxY() + ", " + removed.getMaxZ())); + sender.sendMessage(Component.text("§7Осталось регионов: §f" + currentRegions.size())); + } + + private void updateMineRegions(Mine mine, RegionConfig newConfig) { + var oldConfig = mine.getConfig(); + var newMineConfig = new MineConfig( + oldConfig.name(), + newConfig, + oldConfig.blocks(), + oldConfig.reset(), + oldConfig.rewards(), + oldConfig.teleport(), + oldConfig.ui(), + oldConfig.worldGuard(), + oldConfig.playerSpawn() + ); + + try { + plugin.getMines().updateMineConfig(mine.getName(), newMineConfig); + } catch (Exception e) { + throw new IllegalStateException("Failed to update mine regions", e); + } + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java b/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java new file mode 100644 index 0000000..95fca1f --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/info/RegionCommands.java @@ -0,0 +1,79 @@ +package dev.loki.lomines.command.admin.info; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.util.location.geo.Cuboid; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; + +import java.util.List; + +public class RegionCommands { + + private final LoMinesPlugin plugin; + private final RegionActionHandler actionHandler; + + public RegionCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + this.actionHandler = new RegionActionHandler(plugin); + } + + public void handle(CommandSender sender, String subcommand, String[] args) { + switch (subcommand.toLowerCase()) { + case "regions" -> handleRegions(sender, args); + case "addregion" -> handleAddRegion(sender, args); + case "removeregion" -> handleRemoveRegion(sender, args); + default -> {} + } + } + + private void handleRegions(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm regions ", 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 new file mode 100644 index 0000000..1f51a8d --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/manage/AdminCommands.java @@ -0,0 +1,90 @@ +package dev.loki.lomines.command.admin.manage; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.util.ErrorHandler; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; + +import java.util.Collection; + +public class AdminCommands { + + private final LoMinesPlugin plugin; + private final Mines mines; + private final ErrorHandler errorHandler; + private final MineActionHandler actionHandler; + + public AdminCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + this.mines = plugin.getMines(); + this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); + this.actionHandler = new MineActionHandler(plugin, mines, errorHandler); + } + + public void handle(CommandSender sender, String subcommand, String[] args) { + switch (subcommand.toLowerCase()) { + case "create" -> handleCreate(sender, args); + case "delete" -> handleDelete(sender, args); + case "edit" -> handleEdit(sender, args); + case "reset" -> handleReset(sender, args); + case "reload" -> actionHandler.reload(sender); + case "list" -> handleList(sender); + default -> {} + } + } + + private void handleCreate(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm create ")); + return; + } + actionHandler.create(sender, args[0]); + } + + private void handleDelete(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm delete ")); + return; + } + actionHandler.delete(sender, args[0]); + } + + private void handleEdit(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm edit ")); + return; + } + actionHandler.edit(sender, args[0]); + } + + private void handleReset(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm reset [silent]")); + return; + } + boolean silent = args.length > 1 && (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("silent")); + actionHandler.reset(sender, args[0], silent); + } + + private void handleList(CommandSender sender) { + Collection allMines = mines.getAll(); + if (allMines.isEmpty()) { + sender.sendMessage(Component.text("No mines found")); + return; + } + sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ===")); + for (Mine mine : allMines) { + String status = formatMineStatus(mine); + sender.sendMessage(Component.text("- " + mine.getName() + " " + status)); + } + } + + private String formatMineStatus(Mine mine) { + int blocks = mine.getBlocks(); + int total = mine.getTotalVolume(); + double percent = mine.getPercentFilled(); + return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent); + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java b/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java new file mode 100644 index 0000000..4608feb --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/manage/CopyCommand.java @@ -0,0 +1,92 @@ +package dev.loki.lomines.command.admin.manage; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.data.config.model.MineConfig; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; + +public class CopyCommand { + + private final LoMinesPlugin plugin; + + public CopyCommand(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + public void handle(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(Component.text("Usage: /lm copy ", NamedTextColor.RED)); + return; + } + String fromMineName = args[0]; + String toMineName = args[1]; + + MinePair pair = resolveMines(sender, fromMineName, toMineName); + if (pair == null) { + return; + } + + var sourceConfig = pair.fromMine().getConfig(); + + MineConfig newTargetConfig = buildCopiedConfig(pair.toMine(), sourceConfig); + + try { + plugin.getMines().updateMineConfig(pair.toMine().getName(), newTargetConfig); + } catch (Exception e) { + sender.sendMessage(Component.text("Ошибка сохранения: " + e.getMessage(), NamedTextColor.RED)); + return; + } + + sendCopySummary(sender, pair.fromMine(), pair.toMine(), sourceConfig); + } + + private record MinePair(Mine fromMine, Mine toMine) { + } + + private MinePair resolveMines(CommandSender sender, String fromMineName, String toMineName) { + Mine fromMine = plugin.getMines().find(fromMineName).orElse(null); + if (fromMine == null) { + sender.sendMessage(Component.text("Исходная шахта не найдена: " + fromMineName, NamedTextColor.RED)); + return null; + } + + Mine toMine = plugin.getMines().find(toMineName).orElse(null); + if (toMine == null) { + sender.sendMessage(Component.text("Целевая шахта не найдена: " + toMineName, NamedTextColor.RED)); + return null; + } + + if (fromMine.getName().equalsIgnoreCase(toMine.getName())) { + sender.sendMessage(Component.text("Нельзя копировать шахту саму в себя!", NamedTextColor.RED)); + return null; + } + return new MinePair(fromMine, toMine); + } + + private MineConfig buildCopiedConfig(Mine toMine, MineConfig sourceConfig) { + var targetConfig = toMine.getConfig(); + return MineConfig.builder(toMine.getName()) + .region(targetConfig.region()) + .blocks(sourceConfig.blocks()) + .reset(sourceConfig.reset()) + .rewards(sourceConfig.rewards()) + .teleport(sourceConfig.teleport()) + .ui(sourceConfig.ui()) + .worldGuard(sourceConfig.worldGuard()) + .playerSpawn(sourceConfig.playerSpawn()) + .build(); + } + + private void sendCopySummary(CommandSender sender, Mine fromMine, Mine toMine, MineConfig sourceConfig) { + sender.sendMessage(Component.text("§aКонфигурация скопирована из §f" + fromMine.getName() + + " §aв §f" + toMine.getName())); + sender.sendMessage(Component.text("§7Скопировано:")); + sender.sendMessage(Component.text(" §7• Блоки: §f" + sourceConfig.blocks().weights().size() + " типов")); + sender.sendMessage(Component.text(" §7• Настройки сброса")); + sender.sendMessage(Component.text(" §7• Точки телепорта/спавна")); + sender.sendMessage(Component.text(" §7• Награды: §f" + sourceConfig.rewards().entries().size() + " записей")); + sender.sendMessage(Component.text("§7§oПримечание: регионы и позиции не скопированы")); + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/AdminCommands.java b/src/main/java/dev/loki/lomines/command/admin/manage/MineActionHandler.java similarity index 52% rename from src/main/java/dev/loki/lomines/command/admin/AdminCommands.java rename to src/main/java/dev/loki/lomines/command/admin/manage/MineActionHandler.java index 5560b01..261883d 100644 --- a/src/main/java/dev/loki/lomines/command/admin/AdminCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/manage/MineActionHandler.java @@ -1,45 +1,31 @@ -package dev.loki.lomines.command.admin; +package dev.loki.lomines.command.admin.manage; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.Mine; -import dev.loki.lomines.core.mine.Mines; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.registry.Mines; import dev.loki.lomines.gui.mine.main.MineEditGui; import dev.loki.lomines.util.ErrorHandler; import dev.loki.lomines.util.MessageFormatter; -import dev.lolib.commands.annotation.Arg; -import dev.lolib.commands.annotation.Command; -import dev.lolib.commands.annotation.Subcommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import java.io.IOException; -import java.util.Collection; -/** - * Admin commands for mine management. - * Provides commands for creating, deleting, resetting, reloading, listing, and editing mines. - */ -@Command(value = "lm", permission = "lomines.use") -public class AdminCommands { +public final class MineActionHandler { private final LoMinesPlugin plugin; private final Mines mines; private final ErrorHandler errorHandler; - public AdminCommands(LoMinesPlugin plugin) { + public MineActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { this.plugin = plugin; - this.mines = plugin.getMines(); - this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); + this.mines = mines; + this.errorHandler = errorHandler; } - /** - * Creates a new mine with the given name. - * Usage: /lm create - */ - @Subcommand(value = "create", permission = "lomines.admin.create") - public void create(CommandSender sender, @Arg("name") String name) { + public void create(CommandSender sender, String name) { try { mines.create(name); sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name)); @@ -58,12 +44,7 @@ public class AdminCommands { } } - /** - * Deletes an existing mine. - * Usage: /lm delete - */ - @Subcommand(value = "delete", permission = "lomines.admin.delete") - public void delete(CommandSender sender, @Arg("name") String name) { + public void delete(CommandSender sender, String name) { try { mines.delete(name); sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name)); @@ -75,12 +56,7 @@ public class AdminCommands { } } - /** - * Opens the GUI editor for a mine. - * Usage: /lm edit - */ - @Subcommand(value = "edit", permission = "lomines.admin.edit") - public void edit(CommandSender sender, @Arg("mine") String mineName) { + public void edit(CommandSender sender, String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -95,16 +71,7 @@ public class AdminCommands { MineEditGui.open(plugin, player, mineName); } - /** - * Resets a mine by refilling all its regions. - * Usage: /lm reset [silent] - */ - @Subcommand(value = "reset", permission = "lomines.admin.reset") - public void reset( - CommandSender sender, - @Arg("mine") String mineName, - @Arg(value = "silent", optional = true, defaultValue = "false") boolean silent - ) { + public void reset(CommandSender sender, String mineName, boolean silent) { try { Mine mine = mines.get(mineName); mine.reset(silent); @@ -114,11 +81,6 @@ public class AdminCommands { } } - /** - * Reloads all mine configurations from disk. - * Usage: /lm reload - */ - @Subcommand(value = "reload", permission = "lomines.admin.reload") public void reload(CommandSender sender) { try { mines.getAll().forEach(Mine::stop); @@ -130,32 +92,4 @@ public class AdminCommands { "Failed to reload mines", e); } } - - /** - * Lists all active mines. - * Usage: /lm list - */ - @Subcommand(value = "list", permission = "lomines.admin.list") - public void list(CommandSender sender) { - Collection allMines = mines.getAll(); - - if (allMines.isEmpty()) { - sender.sendMessage(Component.text("No mines found")); - return; - } - - sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ===")); - - for (Mine mine : allMines) { - String status = formatMineStatus(mine); - sender.sendMessage(Component.text("- " + mine.getName() + " " + status)); - } - } - - private String formatMineStatus(Mine mine) { - int blocks = mine.getBlocks(); - int total = mine.getTotalVolume(); - double percent = mine.getPercentFilled(); - return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent); - } } diff --git a/src/main/java/dev/loki/lomines/command/admin/MaskCommands.java b/src/main/java/dev/loki/lomines/command/admin/misc/MaskCommands.java similarity index 64% rename from src/main/java/dev/loki/lomines/command/admin/MaskCommands.java rename to src/main/java/dev/loki/lomines/command/admin/misc/MaskCommands.java index c91fbe5..d647610 100644 --- a/src/main/java/dev/loki/lomines/command/admin/MaskCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/misc/MaskCommands.java @@ -1,23 +1,16 @@ -package dev.loki.lomines.command.admin; +package dev.loki.lomines.command.admin.misc; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.Mines; -import dev.loki.lomines.data.config.loader.common.ConfigLoader; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.data.config.ConfigLoader; import dev.loki.lomines.util.ErrorHandler; import dev.loki.lomines.util.MessageFormatter; -import dev.lolib.commands.annotation.Arg; -import dev.lolib.commands.annotation.Command; -import dev.lolib.commands.annotation.Subcommand; -import dev.lolib.scheduler.Scheduler; + +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import java.io.IOException; -/** - * Mask commands for mine shape management. - * Provides commands for scanning and saving mask positions. - */ -@Command(value = "lm", permission = "lomines.use") public class MaskCommands { private final LoMinesPlugin plugin; @@ -30,9 +23,13 @@ public class MaskCommands { this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); } - @Subcommand(value = "maskscan", permission = "lomines.admin.maskscan") - public void maskScan(CommandSender sender, @Arg("mine") String name) { - Scheduler.get(plugin).run(() -> { + public void handle(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(MessageFormatter.error("Usage: /lm maskscan ")); + return; + } + String name = args[0]; + Bukkit.getScheduler().runTask(plugin, () -> { try { int n = mines.scanAndSaveMask(name); sender.sendMessage(MessageFormatter.success( diff --git a/src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java b/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java similarity index 56% rename from src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java rename to src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java index 5bfdb74..dd974d5 100644 --- a/src/main/java/dev/loki/lomines/command/admin/TeleportCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/misc/TeleportActionHandler.java @@ -1,42 +1,32 @@ -package dev.loki.lomines.command.admin; +package dev.loki.lomines.command.admin.misc; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.Mine; -import dev.loki.lomines.core.mine.Mines; -import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.data.config.model.MineConfig; import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.util.ErrorHandler; import dev.loki.lomines.util.MessageFormatter; -import dev.lolib.commands.annotation.Arg; -import dev.lolib.commands.annotation.Subcommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -/** - * Commands for managing teleport and spawn locations for mines. - */ -class TeleportCommands { +public final class TeleportActionHandler { private final LoMinesPlugin plugin; private final Mines mines; private final ErrorHandler errorHandler; - TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { + public TeleportActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { this.plugin = plugin; this.mines = mines; this.errorHandler = errorHandler; } - /** - * Sets the teleport location for players when mine resets. - * Usage: /lm setteleport - */ - @Subcommand(value = "setteleport", permission = "lomines.admin.setteleport") - void setTeleport(CommandSender sender, @Arg("mine") String mineName) { + public void setTeleport(CommandSender sender, String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -50,18 +40,7 @@ class TeleportCommands { } 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")); @@ -72,12 +51,7 @@ class TeleportCommands { } } - /** - * Sets the safe spawn location for stuck players after mine reset. - * Usage: /lm setspawn - */ - @Subcommand(value = "setspawn", permission = "lomines.admin.setspawn") - void setSpawn(CommandSender sender, @Arg("mine") String mineName) { + public void setSpawn(CommandSender sender, String mineName) { if (!(sender instanceof Player player)) { sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED)); return; @@ -91,18 +65,7 @@ class TeleportCommands { } 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")); @@ -114,12 +77,7 @@ class TeleportCommands { } } - /** - * Clears the spawn location for stuck players. - * Usage: /lm clearspawn - */ - @Subcommand(value = "clearspawn", permission = "lomines.admin.setspawn") - void clearSpawn(CommandSender sender, @Arg("mine") String mineName) { + public void clearSpawn(CommandSender sender, String mineName) { try { Mine mine = mines.find(mineName).orElse(null); if (mine == null) { @@ -128,16 +86,7 @@ class TeleportCommands { } 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")); @@ -148,7 +97,33 @@ class TeleportCommands { } } - private String formatLocation(Location loc) { + private MineConfig withTeleport(MineConfig config, TeleportConfig teleport) { + return MineConfig.builder(config.name()) + .region(config.region()) + .blocks(config.blocks()) + .reset(config.reset()) + .rewards(config.rewards()) + .teleport(teleport) + .ui(config.ui()) + .worldGuard(config.worldGuard()) + .playerSpawn(config.playerSpawn()) + .build(); + } + + private MineConfig withPlayerSpawn(MineConfig config, PlayerSpawnConfig spawn) { + return MineConfig.builder(config.name()) + .region(config.region()) + .blocks(config.blocks()) + .reset(config.reset()) + .rewards(config.rewards()) + .teleport(config.teleport()) + .ui(config.ui()) + .worldGuard(config.worldGuard()) + .playerSpawn(spawn) + .build(); + } + + public String formatLocation(Location loc) { return String.format("%.1f, %.1f, %.1f in %s", loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName()); } diff --git a/src/main/java/dev/loki/lomines/command/admin/misc/TeleportCommands.java b/src/main/java/dev/loki/lomines/command/admin/misc/TeleportCommands.java new file mode 100644 index 0000000..77f7ada --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/misc/TeleportCommands.java @@ -0,0 +1,30 @@ +package dev.loki.lomines.command.admin.misc; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.util.ErrorHandler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; + +public class TeleportCommands { + + private final TeleportActionHandler actionHandler; + + public TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) { + this.actionHandler = new TeleportActionHandler(plugin, mines, errorHandler); + } + + public void handle(CommandSender sender, String subcommand, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm " + subcommand + " ", NamedTextColor.RED)); + return; + } + switch (subcommand.toLowerCase()) { + case "setteleport" -> actionHandler.setTeleport(sender, args[0]); + case "setspawn" -> actionHandler.setSpawn(sender, args[0]); + case "clearspawn" -> actionHandler.clearSpawn(sender, args[0]); + default -> {} + } + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/HologramCommands.java b/src/main/java/dev/loki/lomines/command/admin/stats/HologramCommands.java similarity index 58% rename from src/main/java/dev/loki/lomines/command/admin/HologramCommands.java rename to src/main/java/dev/loki/lomines/command/admin/stats/HologramCommands.java index 57ee1ce..abb9a20 100644 --- a/src/main/java/dev/loki/lomines/command/admin/HologramCommands.java +++ b/src/main/java/dev/loki/lomines/command/admin/stats/HologramCommands.java @@ -1,35 +1,30 @@ -package dev.loki.lomines.command.admin; +package dev.loki.lomines.command.admin.stats; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.core.mine.model.Mine; import dev.loki.lomines.integration.hologram.HologramManager; -import dev.lolilb.commands.annotation.Arg; -import dev.lolilb.commands.annotation.Subcommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -/** - * Commands for managing holograms above mines. - */ -class HologramCommands { +public class HologramCommands { private final LoMinesPlugin plugin; private final HologramManager hologramManager; - HologramCommands(LoMinesPlugin plugin) { + public HologramCommands(LoMinesPlugin plugin) { this.plugin = plugin; this.hologramManager = plugin.getHologramManager(); } - /** - * Toggles hologram for a mine. - * Usage: /lm hologram [on|off] - */ - @Subcommand(value = "hologram", permission = "lomines.admin.hologram") - void hologram(CommandSender sender, @Arg("mine") String mineName, - @Arg(value = "state", optional = true) String state) { + public void handle(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm hologram [on|off]", NamedTextColor.RED)); + return; + } + String mineName = args[0]; + String state = args.length > 1 ? args[1] : null; + Mine mine = plugin.getMines().find(mineName).orElse(null); if (mine == null) { sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); @@ -43,10 +38,9 @@ class HologramCommands { boolean enable; if (state == null) { - // Toggle enable = hologramManager.toggleHologram(mine); } else { - enable = state.equalsIgnoreCase("on") || state.equalsIgnoreCase("true") || state.equalsIgnoreCase("enable"); + enable = "on".equalsIgnoreCase(state) || "true".equalsIgnoreCase(state) || "enable".equalsIgnoreCase(state); if (enable) { hologramManager.createMineHologram(mine); } else { @@ -55,6 +49,6 @@ class HologramCommands { } sender.sendMessage(Component.text( - "Голограмма для шахты " + mineName + " " + (enable ? "§aвключена" : "§cвыключена"))); + "Голограмма для шахты " + mineName + " " + (enable ? "§aвключена" : "§cвыключена"))); } } diff --git a/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java b/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java new file mode 100644 index 0000000..f86c39b --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/stats/LeaderboardRenderer.java @@ -0,0 +1,121 @@ +package dev.loki.lomines.command.admin.stats; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.data.stats.model.LeaderboardEntry; +import dev.loki.lomines.util.ErrorHandler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.UUID; + +public final class LeaderboardRenderer { + + private LeaderboardRenderer() { + } + + public static void sendTopRows(CommandSender sender, List rows) { + if (rows.isEmpty()) { + sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY)); + return; + } + for (int i = 0; i < rows.size(); i++) { + LeaderboardEntry e = rows.get(i); + String name = formatPlayerName(e.playerId()); + sender.sendMessage(Component.text( + (i + 1) + ". " + name + " \u2014 " + e.count(), + NamedTextColor.WHITE)); + } + } + + public static String formatPlayerName(UUID id) { + String name = Bukkit.getOfflinePlayer(id).getName(); + return name != null ? name : id.toString(); + } + + public static int clampLimit(int limit) { + return Math.max(1, Math.min(50, limit)); + } + + public static Integer tryParsePositiveInt(String s) { + try { + int v = Integer.parseInt(s); + return v > 0 ? v : null; + } catch (NumberFormatException e) { + return null; + } + } + + public static UUID resolvePlayerUuid(CommandSender sender, String targetName, ErrorHandler errorHandler) { + String name = targetName == null ? "" : targetName.trim(); + if (name.isEmpty()) { + if (!(sender instanceof Player self)) { + errorHandler.sendError(sender, "Укажите игрока: /lm stats <ник>"); + return null; + } + return self.getUniqueId(); + } else { + if (!sender.hasPermission("lomines.stats.others")) { + errorHandler.handlePermissionDenied(sender, "смотреть чужую статистику"); + return null; + } + Player online = Bukkit.getPlayerExact(name); + if (online != null) { + return online.getUniqueId(); + } else { + OfflinePlayer off = Bukkit.getOfflinePlayer(name); + return off.getUniqueId(); + } + } + } + + public static void renderTopCommand(CommandSender sender, String arg1, String arg2, Mines mines, LoMinesPlugin plugin) { + TopRequest req = parseTopRequest(arg1, arg2); + if (req.mineName() != null) { + renderMineTop(sender, req.mineName(), req.limit(), mines, plugin); + return; + } + List rows = plugin.getStatsManager().getLeaderboard().getTopTotal(req.limit()); + sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD)); + sendTopRows(sender, rows); + } + + private record TopRequest(int limit, String mineName) { + } + + private static TopRequest parseTopRequest(String arg1, String arg2) { + String a = arg1 == null ? "" : arg1.trim(); + String b = arg2 == null ? "" : arg2.trim(); + if (a.isEmpty()) { + return new TopRequest(clampLimit(10), null); + } + if (b.isEmpty()) { + Integer asLimit = tryParsePositiveInt(a); + if (asLimit != null) { + return new TopRequest(clampLimit(asLimit), null); + } + return new TopRequest(clampLimit(10), a); + } + Integer limParsed = tryParsePositiveInt(b); + return new TopRequest(clampLimit(limParsed != null ? limParsed : 10), a); + } + + private static void renderMineTop(CommandSender sender, String mineName, int limit, Mines mines, LoMinesPlugin plugin) { + var found = mines.find(mineName); + if (found.isEmpty()) { + sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + Mine m = found.get(); + String key = m.getName(); + List rows = plugin.getStatsManager().getLeaderboard().getTopByMine(key, limit); + sender.sendMessage(Component.text("=== Топ по шахте \u00ab" + key + "\u00bb ===", NamedTextColor.GOLD)); + sendTopRows(sender, rows); + } +} diff --git a/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java b/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java new file mode 100644 index 0000000..4cd0bb0 --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/stats/StatsCommands.java @@ -0,0 +1,80 @@ +package dev.loki.lomines.command.admin.stats; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.registry.Mines; +import dev.loki.lomines.data.stats.model.PlayerStats; +import dev.loki.lomines.data.stats.service.StatsManager; +import dev.loki.lomines.util.ErrorHandler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; + +import java.util.Map; +import java.util.UUID; + +public class StatsCommands { + + private final LoMinesPlugin plugin; + private final Mines mines; + private final ErrorHandler errorHandler; + + public StatsCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + this.mines = plugin.getMines(); + this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); + } + + public void handle(CommandSender sender, String subcommand, String[] args) { + switch (subcommand.toLowerCase()) { + case "stats" -> handleStats(sender, args); + case "top" -> { + if (isStatsDisabled(sender)) { + return; + } + String arg1 = args.length > 0 ? args[0] : ""; + String arg2 = args.length > 1 ? args[1] : ""; + LeaderboardRenderer.renderTopCommand(sender, arg1, arg2, mines, plugin); + } + default -> {} + } + } + + private void handleStats(CommandSender sender, String[] args) { + if (isStatsDisabled(sender)) { + return; + } + String targetName = args.length > 0 ? args[0] : ""; + UUID uuid = LeaderboardRenderer.resolvePlayerUuid(sender, targetName, errorHandler); + if (uuid == null) { + return; + } + + StatsManager sm = plugin.getStatsManager(); + PlayerStats ps = sm.getOrCreate(uuid); + String displayName = LeaderboardRenderer.formatPlayerName(uuid); + sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE)); + int pos = sm.getLeaderboard().getPosition(uuid); + if (pos > 0) { + sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY)); + } + Map byMine = ps.getMineStatsSnapshot(); + if (byMine.isEmpty()) { + return; + } + sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY)); + byMine.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(12) + .forEach(e -> sender.sendMessage(Component.text( + " " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA))); + } + + private boolean isStatsDisabled(CommandSender sender) { + if (!plugin.getConfig().getBoolean("statistics-enabled", true)) { + sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED)); + return true; + } + return false; + } +} diff --git a/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java index 0ac51f4..23400ee 100644 --- a/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java +++ b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java @@ -4,31 +4,26 @@ import dev.loki.lomines.LoMinesPlugin; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; -/** - * Tab completer for LoMines commands. - * Provides auto-completion suggestions for all subcommands and arguments. - */ public class LoMinesTabCompleter implements TabCompleter { - private final LoMinesPlugin plugin; - private static final List ALL_COMMANDS = List.of( "create", "delete", "reset", "reload", "list", "wand", "group", "stats", "top", "maskscan", "edit", "setteleport", "setspawn", - "clearspawn", "help" + "clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help" ); - private static final List BOOLEAN_VALUES = List.of("true", "false"); - private static final List TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50"); + private final LoMinesPlugin plugin; + private final SubcommandCompleter completer; + private final PermissionPredicate permission; public LoMinesTabCompleter(LoMinesPlugin plugin) { this.plugin = plugin; + this.completer = new SubcommandCompleter(plugin); + this.permission = new PermissionPredicate(); } @Override @@ -41,130 +36,27 @@ public class LoMinesTabCompleter implements TabCompleter { return switch (args.length) { case 1 -> completeSubcommands(sender, partial); - case 2 -> completeSecondArg(sender, args[0].toLowerCase(), partial); - case 3 -> completeThirdArg(args[0].toLowerCase(), args[1], partial); + case 2 -> completer.completeSecondArg(sender, args[0].toLowerCase(), partial); + case 3 -> completer.completeThirdArg(args[0].toLowerCase(), args[1], partial); default -> new ArrayList<>(); }; } private boolean isLoMinesCommand(String name) { - return name.equalsIgnoreCase("lm") || - name.equalsIgnoreCase("lomines") || - name.equalsIgnoreCase("mine") || - name.equalsIgnoreCase("mines"); + return "lm".equalsIgnoreCase(name) || + "lomines".equalsIgnoreCase(name) || + "mine".equalsIgnoreCase(name) || + "mines".equalsIgnoreCase(name); } private List completeSubcommands(CommandSender sender, String partial) { List completions = new ArrayList<>(); for (String cmd : ALL_COMMANDS) { - if (cmd.startsWith(partial) && hasPermission(sender, cmd)) { + if (cmd.startsWith(partial) && permission.hasPermission(sender, cmd)) { completions.add(cmd); } } return completions; } - - private List completeSecondArg(CommandSender sender, String subcommand, String partial) { - return switch (subcommand) { - case "create" -> completeCreate(partial); - case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn" -> - filterStartsWith(getMineNames(), partial); - case "reset" -> completeReset(partial); - case "stats" -> completeStats(sender, partial); - case "top" -> completeTopFirstArg(partial); - default -> new ArrayList<>(); - }; - } - - private List completeThirdArg(String subcommand, String arg2, String partial) { - return switch (subcommand) { - case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial); - case "top" -> completeTopSecondArg(arg2, partial); - default -> new ArrayList<>(); - }; - } - - private List completeCreate(String partial) { - if (partial.isEmpty()) { - return new ArrayList<>(List.of("")); - } - return new ArrayList<>(); - } - - private List completeReset(String partial) { - List mines = filterStartsWith(getMineNames(), partial); - if (partial.isEmpty() || "true".startsWith(partial) || "false".startsWith(partial)) { - mines.addAll(filterStartsWith(BOOLEAN_VALUES, partial)); - } - return mines; - } - - private List completeStats(CommandSender sender, String partial) { - if (!sender.hasPermission("lomines.stats.others")) { - return new ArrayList<>(); - } - return filterStartsWith(getOnlinePlayerNames(), partial); - } - - private List completeTopFirstArg(String partial) { - List completions = new ArrayList<>(); - - completions.addAll(filterStartsWith(getMineNames(), partial)); - - if (isNumericPartial(partial)) { - completions.addAll(filterStartsWith(TOP_LIMITS, partial)); - } - - return completions; - } - - private List completeTopSecondArg(String firstArg, String partial) { - boolean firstArgWasMine = plugin.getMines().find(firstArg).isPresent(); - - if (firstArgWasMine && isNumericPartial(partial)) { - return filterStartsWith(TOP_LIMITS, partial); - } - - if (!firstArgWasMine && isNumericPartial(firstArg)) { - return filterStartsWith(getMineNames(), partial); - } - - return new ArrayList<>(); - } - - private boolean isNumericPartial(String partial) { - return partial.isEmpty() || partial.matches("\\d*"); - } - - private List filterStartsWith(List options, String partial) { - return options.stream() - .filter(s -> s.toLowerCase().startsWith(partial)) - .collect(Collectors.toList()); - } - - private List getMineNames() { - return plugin.getMines().getAll().stream() - .map(mine -> mine.getName()) - .collect(Collectors.toList()); - } - - private List getOnlinePlayerNames() { - return plugin.getServer().getOnlinePlayers().stream() - .map(Player::getName) - .collect(Collectors.toList()); - } - - private boolean hasPermission(CommandSender sender, String cmd) { - return switch (cmd) { - case "create", "delete", "reload", "list", "maskscan", "edit" -> - sender.hasPermission("lomines.admin"); - case "reset" -> sender.hasPermission("lomines.admin.reset"); - case "setteleport" -> sender.hasPermission("lomines.admin.setteleport"); - case "setspawn", "clearspawn" -> sender.hasPermission("lomines.admin.setspawn"); - case "wand", "group" -> sender.hasPermission("lomines.admin.wand"); - case "stats", "top" -> sender.hasPermission("lomines.stats"); - case "help" -> sender.hasPermission("lomines.use"); - default -> true; - }; - } } + diff --git a/src/main/java/dev/loki/lomines/command/common/PermissionPredicate.java b/src/main/java/dev/loki/lomines/command/common/PermissionPredicate.java new file mode 100644 index 0000000..14c2ced --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/common/PermissionPredicate.java @@ -0,0 +1,23 @@ +package dev.loki.lomines.command.common; + +import org.bukkit.command.CommandSender; + +public final class PermissionPredicate { + + public boolean hasPermission(CommandSender sender, String cmd) { + return switch (cmd) { + case "create", "delete", "reload", "list", "maskscan", "edit", "info" -> + sender.hasPermission("lomines.admin"); + case "reset" -> sender.hasPermission("lomines.admin.reset"); + case "setteleport" -> sender.hasPermission("lomines.admin.setteleport"); + case "setspawn", "clearspawn" -> sender.hasPermission("lomines.admin.setspawn"); + case "wand", "group" -> sender.hasPermission("lomines.admin.wand"); + case "stats", "top" -> sender.hasPermission("lomines.stats"); + case "tp" -> sender.hasPermission("lomines.teleport"); + case "copy" -> sender.hasPermission("lomines.admin.copy"); + case "regions", "addregion", "removeregion" -> sender.hasPermission("lomines.admin.regions"); + case "help" -> sender.hasPermission("lomines.use"); + default -> true; + }; + } +} diff --git a/src/main/java/dev/loki/lomines/command/common/SubcommandCompleter.java b/src/main/java/dev/loki/lomines/command/common/SubcommandCompleter.java new file mode 100644 index 0000000..2bc1f26 --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/common/SubcommandCompleter.java @@ -0,0 +1,130 @@ +package dev.loki.lomines.command.common; + +import dev.loki.lomines.LoMinesPlugin; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public final class SubcommandCompleter { + + private static final List BOOLEAN_VALUES = List.of("true", "false"); + private static final List TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50"); + + private final LoMinesPlugin plugin; + + public SubcommandCompleter(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + public List completeSecondArg(CommandSender sender, String subcommand, String partial) { + return switch (subcommand) { + case "create" -> completeCreate(partial); + case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn", "info", + "regions", "addregion" -> filterStartsWith(getMineNames(), partial); + case "reset" -> completeReset(partial); + case "stats" -> completeStats(sender, partial); + case "top" -> completeTopFirstArg(partial); + case "tp" -> completeTeleport(sender, partial); + case "copy" -> filterStartsWith(getMineNames(), partial); + case "removeregion" -> filterStartsWith(getMineNames(), partial); + default -> new ArrayList<>(); + }; + } + + public List completeThirdArg(String subcommand, String arg2, String partial) { + return switch (subcommand) { + case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial); + case "top" -> completeTopSecondArg(arg2, partial); + case "copy" -> filterStartsWith(getMineNames(), partial); + case "removeregion" -> completeRegionIndex(arg2, partial); + default -> new ArrayList<>(); + }; + } + + private List completeRegionIndex(String mineName, String partial) { + var mine = plugin.getMines().find(mineName); + if (mine.isEmpty()) { + return new ArrayList<>(); + } + int regionCount = mine.get().getRegions().size(); + List indices = new ArrayList<>(); + for (int i = 1; i <= regionCount; i++) { + indices.add(String.valueOf(i)); + } + return filterStartsWith(indices, partial); + } + + private List completeCreate(String partial) { + if (partial.isEmpty()) { + return new ArrayList<>(List.of("")); + } + return new ArrayList<>(); + } + + private List completeReset(String partial) { + List mines = filterStartsWith(getMineNames(), partial); + if (partial.isEmpty() || "true".startsWith(partial) || "false".startsWith(partial)) { + mines.addAll(filterStartsWith(BOOLEAN_VALUES, partial)); + } + return mines; + } + + private List completeStats(CommandSender sender, String partial) { + if (!sender.hasPermission("lomines.stats.others")) { + return new ArrayList<>(); + } + return filterStartsWith(getOnlinePlayerNames(), partial); + } + + private List completeTopFirstArg(String partial) { + List completions = new ArrayList<>(); + completions.addAll(filterStartsWith(getMineNames(), partial)); + if (isNumericPartial(partial)) { + completions.addAll(filterStartsWith(TOP_LIMITS, partial)); + } + return completions; + } + + private List completeTopSecondArg(String firstArg, String partial) { + boolean firstArgWasMine = plugin.getMines().find(firstArg).isPresent(); + if (firstArgWasMine && isNumericPartial(partial)) { + return filterStartsWith(TOP_LIMITS, partial); + } + if (!firstArgWasMine && isNumericPartial(firstArg)) { + return filterStartsWith(getMineNames(), partial); + } + return new ArrayList<>(); + } + + private List completeTeleport(CommandSender sender, String partial) { + if (!sender.hasPermission("lomines.teleport")) { + return new ArrayList<>(); + } + return filterStartsWith(getMineNames(), partial); + } + + private boolean isNumericPartial(String partial) { + return partial.isEmpty() || partial.matches("\\d*"); + } + + public List filterStartsWith(List options, String partial) { + return options.stream() + .filter(s -> s.toLowerCase().startsWith(partial)) + .collect(Collectors.toList()); + } + + public List getMineNames() { + return plugin.getMines().getAll().stream() + .map(mine -> mine.getName()) + .collect(Collectors.toList()); + } + + private List getOnlinePlayerNames() { + return plugin.getServer().getOnlinePlayers().stream() + .map(Player::getName) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java b/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java index ccf9178..ea1bd63 100644 --- a/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java +++ b/src/main/java/dev/loki/lomines/command/player/PlayerCommands.java @@ -3,19 +3,11 @@ package dev.loki.lomines.command.player; import dev.loki.lomines.LoMinesPlugin; import dev.loki.lomines.util.ErrorHandler; import dev.loki.lomines.wand.group.GroupWandItem; -import dev.lolib.commands.annotation.Arg; -import dev.lolib.commands.annotation.Command; -import dev.lolib.commands.annotation.Subcommand; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -/** - * Player commands for mine interaction. - * Provides commands for wand and group management. - */ -@Command(value = "lm", permission = "lomines.use") public class PlayerCommands { private final LoMinesPlugin plugin; @@ -26,26 +18,32 @@ public class PlayerCommands { this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] "); } - @Subcommand(value = "wand", permission = "lomines.admin.wand") - public void wand(CommandSender sender) { - if (!(sender instanceof Player player)) { - errorHandler.sendError(sender, "Только для игрока."); - return; + public void handle(CommandSender sender, String subcommand, String[] args) { + switch (subcommand.toLowerCase()) { + case "wand" -> { + if (!(sender instanceof Player player)) { + errorHandler.sendError(sender, "Только для игрока."); + return; + } + player.getInventory().addItem(GroupWandItem.create(plugin)); + player.sendMessage(Component.text( + "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", + NamedTextColor.GREEN)); + } + case "group" -> { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm group ", NamedTextColor.RED)); + return; + } + if (!(sender instanceof Player player)) { + errorHandler.sendError(sender, "Префикс задаётся в игре."); + return; + } + plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(args[0]); + String p = args[0].trim(); + player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA)); + } + default -> {} } - player.getInventory().addItem(GroupWandItem.create(plugin)); - player.sendMessage(Component.text( - "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", - NamedTextColor.GREEN)); - } - - @Subcommand(value = "group", permission = "lomines.admin.wand") - public void group(CommandSender sender, @Arg("prefix") String prefix) { - if (!(sender instanceof Player player)) { - errorHandler.sendError(sender, "Префикс задаётся в игре."); - return; - } - plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix); - String p = prefix.trim(); - player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA)); } } diff --git a/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java b/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java new file mode 100644 index 0000000..75d0d8d --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/player/TeleportCommand.java @@ -0,0 +1,63 @@ +package dev.loki.lomines.command.player; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.util.location.geo.Cuboid; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class TeleportCommand { + + private final LoMinesPlugin plugin; + + public TeleportCommand(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + public void handle(CommandSender sender, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Usage: /lm tp ", NamedTextColor.RED)); + return; + } + String mineName = args[0]; + + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED)); + return; + } + + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + + Location target = resolveTarget(player, mine); + if (target == null) { + return; + } + + player.teleport(target); + player.sendMessage(Component.text("§aТелепортировано к шахте §f" + mine.getName() + + " §a(§f" + String.format("%.0f, %.0f, %.0f", target.getX(), target.getY(), target.getZ()) + "§a)")); + } + + private Location resolveTarget(Player player, Mine mine) { + var config = mine.getConfig(); + if (config.teleport().enabled() && config.teleport().getLocation().isPresent()) { + return config.teleport().getLocation().get(); + } + if (!mine.getRegions().isEmpty()) { + Cuboid region = mine.getRegions().get(0); + double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5; + double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5; + double centerY = region.getMaxY() + 1; + return new Location(region.getWorld(), centerX, centerY, centerZ); + } + player.sendMessage(Component.text("У шахты нет настроенной точки телепорта!", NamedTextColor.RED)); + return null; + } +} diff --git a/src/main/java/dev/loki/lomines/core/common/package-info.java b/src/main/java/dev/loki/lomines/core/common/package-info.java 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/Mine.java b/src/main/java/dev/loki/lomines/core/mine/model/Mine.java similarity index 59% rename from src/main/java/dev/loki/lomines/core/mine/Mine.java rename to src/main/java/dev/loki/lomines/core/mine/model/Mine.java index 800faaa..ad4aea0 100644 --- a/src/main/java/dev/loki/lomines/core/mine/Mine.java +++ b/src/main/java/dev/loki/lomines/core/mine/model/Mine.java @@ -1,31 +1,28 @@ -package dev.loki.lomines.core.mine; +package dev.loki.lomines.core.mine.model; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.block.BlockSetter; -import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.data.config.model.MineConfig; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.data.config.block.FillMode; -import dev.loki.lomines.handler.ActionBarHandler; -import dev.loki.lomines.handler.MineBlockHandler; -import dev.loki.lomines.handler.MineResetHandler; +import dev.loki.lomines.handler.ui.ActionBarHandler; +import dev.loki.lomines.handler.block.MineBlockHandler; +import dev.loki.lomines.handler.reset.MineResetHandler; import dev.loki.lomines.util.location.BlockKeys; -import dev.loki.lomines.util.location.Cuboid; +import dev.loki.lomines.util.location.geo.Cuboid; import dev.loki.lomines.util.location.LocationParser; -import dev.lolib.scheduler.ScheduledTask; -import dev.lolib.scheduler.Scheduler; +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Player; -import java.util.*; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; -/** - * Represents a single mine with its configuration and state. - * Thread-safe implementation using AtomicInteger for mutable state. - * Updated for new section-based configuration (v2). - */ public final class Mine { private final String name; @@ -33,14 +30,12 @@ public final class Mine { private final MineConfig config; private final BlockSetter blockSetter; private final LoMinesPlugin plugin; - private final int totalVolume; - private final Set maskBlockKeys; - private final AtomicInteger blocks; - private final AtomicInteger ticks; + private final MineState state; private final MineResetHandler resetHandler; private final MineBlockHandler blockHandler; private final ActionBarHandler actionBarHandler; - private ScheduledTask actionBarTask; + private BukkitTask actionBarTask; + private final Set maskBlockKeys; public Mine(String name, MineConfig config, LoMinesPlugin plugin) { this.name = name; @@ -49,30 +44,21 @@ public final class Mine { this.regions = config.region().regions(); this.blockSetter = createBlockSetter(config, plugin); this.maskBlockKeys = initMaskBlockKeys(config); - this.totalVolume = config.blocks().fillMode() == FillMode.MASK - ? maskBlockKeys.size() - : config.region().totalVolume(); - this.blocks = new AtomicInteger(Math.max(0, totalVolume)); - this.ticks = new AtomicInteger(0); + this.state = new MineState(config); this.resetHandler = new MineResetHandler(this, plugin); this.blockHandler = new MineBlockHandler(this, plugin); this.actionBarHandler = new ActionBarHandler(this); } private Set initMaskBlockKeys(MineConfig config) { - if (config.blocks().fillMode() != FillMode.MASK) { + if (config.blocks().fillMode() != FillMode.MASK || config.blocks().mask() == null) { return Set.of(); } - Set keys = new HashSet<>(); - if (config.blocks().mask() != null) { - for (String pos : config.blocks().mask().positions().keySet()) { - Location loc = LocationParser.parseSimple(pos); - if (loc != null) { - keys.add(BlockKeys.key(loc)); - } - } - } - return keys; + return config.blocks().mask().positions().keySet().stream() + .map(LocationParser::parseSimple) + .filter(Objects::nonNull) + .map(BlockKeys::key) + .collect(Collectors.toSet()); } public void start() { @@ -99,18 +85,15 @@ public final class Mine { } public boolean appliesToBlock(Location location) { - if (!contains(location)) return false; - if (config.blocks().fillMode() != FillMode.MASK) return true; - return maskBlockKeys.contains(BlockKeys.key(location)); + return contains(location) && (config.blocks().fillMode() != FillMode.MASK || maskBlockKeys.contains(BlockKeys.key(location))); } public int getTotalVolume() { - return totalVolume; + return state.getTotalVolume(); } public double getPercentFilled() { - if (totalVolume == 0) return 0.0; - return (double) blocks.get() / totalVolume * 100.0; + return state.getPercentFilled(); } private BlockSetter createBlockSetter(MineConfig config, LoMinesPlugin plugin) { @@ -120,17 +103,16 @@ public final class Mine { } BlockKey firstKey = weights.keySet().iterator().next(); return switch (firstKey) { - case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("Oraxen integration is currently disabled"); - case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("ItemsAdder integration is currently disabled"); + case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("External plugins disabled"); + case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("External plugins disabled"); case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin); }; } private void startActionBarTask() { - if (!config.ui().actionBarEnabled()) return; - actionBarTask = Scheduler.get(plugin).runTimer(() -> { - actionBarHandler.sendToNearbyPlayers(); - }, 10L, 10L); + if (config.ui().actionBarEnabled()) { + actionBarTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> actionBarHandler.sendToNearbyPlayers(), 10L, 10L); + } } public String getName() { @@ -149,19 +131,19 @@ public final class Mine { return regions; } - public AtomicInteger getBlocksAtomic() { - return blocks; - } - public int getBlocks() { - return blocks.get(); + return state.getBlocks(); } public int getTicks() { - return ticks.get(); + return state.getTicks(); + } + + public AtomicInteger getBlocksAtomic() { + return state.getBlocksAtomic(); } public AtomicInteger getTicksAtomic() { - return ticks; + return state.getTicksAtomic(); } } diff --git a/src/main/java/dev/loki/lomines/core/mine/model/MineState.java b/src/main/java/dev/loki/lomines/core/mine/model/MineState.java new file mode 100644 index 0000000..99b5e3f --- /dev/null +++ b/src/main/java/dev/loki/lomines/core/mine/model/MineState.java @@ -0,0 +1,82 @@ +package dev.loki.lomines.core.mine.model; + +import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.data.config.block.FillMode; +import dev.loki.lomines.util.location.BlockKeys; +import dev.loki.lomines.util.location.LocationParser; +import org.bukkit.Location; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +public final class MineState { + + private final AtomicInteger blocks; + private final AtomicInteger ticks; + private final int totalVolume; + private final Set maskBlockKeys; + + public MineState(MineConfig config) { + this.maskBlockKeys = initMaskBlockKeys(config); + this.totalVolume = config.blocks().fillMode() == FillMode.MASK + ? maskBlockKeys.size() + : config.region().totalVolume(); + this.blocks = new AtomicInteger(Math.max(0, totalVolume)); + this.ticks = new AtomicInteger(0); + } + + private static Set initMaskBlockKeys(MineConfig config) { + if (config.blocks().fillMode() != FillMode.MASK) { + return Set.of(); + } + Set keys = new HashSet<>(); + if (config.blocks().mask() != null) { + for (String pos : config.blocks().mask().positions().keySet()) { + Location loc = LocationParser.parseSimple(pos); + if (loc != null) { + keys.add(BlockKeys.key(loc)); + } + } + } + return keys; + } + + public int getTotalVolume() { + return totalVolume; + } + + public double getPercentFilled() { + if (totalVolume == 0) { + return 0.0; + } + return (double) blocks.get() / totalVolume * 100.0; + } + + public AtomicInteger getBlocksAtomic() { + return blocks; + } + + public int getBlocks() { + return blocks.get(); + } + + public int getTicks() { + return ticks.get(); + } + + public AtomicInteger getTicksAtomic() { + return ticks; + } + + public Set getMaskBlockKeys() { + return maskBlockKeys; + } + + public boolean appliesToBlock(Location location) { + if (maskBlockKeys.isEmpty()) { + return true; + } + return maskBlockKeys.contains(BlockKeys.key(location)); + } +} diff --git a/src/main/java/dev/loki/lomines/core/mine/registry/MineFinder.java b/src/main/java/dev/loki/lomines/core/mine/registry/MineFinder.java new file mode 100644 index 0000000..ffef0ef --- /dev/null +++ b/src/main/java/dev/loki/lomines/core/mine/registry/MineFinder.java @@ -0,0 +1,33 @@ +package dev.loki.lomines.core.mine.registry; + +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.service.MineRepository; +import org.bukkit.Location; + +import java.util.Collection; +import java.util.Optional; + +public final class MineFinder { + + private final MineRepository repository; + + public MineFinder(MineRepository repository) { + this.repository = repository; + } + + public Mine get(String name) { + return repository.get(name); + } + + public Optional find(String name) { + return repository.find(name); + } + + public Collection getAll() { + return repository.getAll(); + } + + public Optional findByLocation(Location location) { + return repository.findByLocation(location); + } +} diff --git a/src/main/java/dev/loki/lomines/core/mine/Mines.java b/src/main/java/dev/loki/lomines/core/mine/registry/Mines.java similarity index 63% rename from src/main/java/dev/loki/lomines/core/mine/Mines.java rename to src/main/java/dev/loki/lomines/core/mine/registry/Mines.java index 6c08932..5b55f1c 100644 --- a/src/main/java/dev/loki/lomines/core/mine/Mines.java +++ b/src/main/java/dev/loki/lomines/core/mine/registry/Mines.java @@ -1,23 +1,20 @@ -package dev.loki.lomines.core.mine; +package dev.loki.lomines.core.mine.registry; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.common.MineLoader; -import dev.loki.lomines.core.service.MaskScanService; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.service.MineLoader; +import dev.loki.lomines.core.service.mask.MaskScanService; import dev.loki.lomines.core.service.MineFileManager; import dev.loki.lomines.core.service.MineRepository; -import dev.loki.lomines.data.config.loader.common.ConfigLoader; -import dev.loki.lomines.data.config.MineConfig; -import dev.loki.lomines.integration.worldguard.WorldGuardRegionService; +import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.integration.worldguard.region.WorldGuardRegionService; import org.bukkit.Location; import java.io.IOException; import java.util.Collection; import java.util.Optional; -/** - * Registry for all active mines. - * Manages the lifecycle of mines including loading, creating, and deleting. - */ public final class Mines { private final LoMinesPlugin plugin; @@ -26,6 +23,7 @@ public final class Mines { private final MineLoader loader; private final MaskScanService maskScanService; private final WorldGuardRegionService worldGuardService; + private final MineFinder finder; public Mines(LoMinesPlugin plugin) { this.plugin = plugin; @@ -34,6 +32,7 @@ public final class Mines { this.loader = new MineLoader(plugin, fileManager, repository); this.maskScanService = new MaskScanService(repository, fileManager); this.worldGuardService = new WorldGuardRegionService(plugin); + this.finder = new MineFinder(repository); } public void loadAll() throws IOException { @@ -45,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); } @@ -64,7 +64,7 @@ public final class Mines { worldGuardService.createRegion(name, config); repository.createAndStart(name, config); - plugin.loLogger().info("Created mine: " + name); + plugin.getLogger().info("Created mine: " + name); } public void delete(String name) throws IOException { @@ -79,39 +79,65 @@ public final class Mines { } public Mine get(String name) { - return repository.get(name); + return finder.get(name); } public Optional find(String name) { - return repository.find(name); + return finder.find(name); } public Collection getAll() { - return repository.getAll(); + return finder.getAll(); } public Optional findByLocation(Location location) { - return repository.findByLocation(location); + return finder.findByLocation(location); } public int scanAndSaveMask(String mineName) throws IOException, ConfigLoader.ConfigLoadException { int count = maskScanService.scanAndSave(mineName); - plugin.loLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)"); + plugin.getLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)"); return count; } public void reloadMine(String name) throws IOException, ConfigLoader.ConfigLoadException { - Mine oldMine = repository.find(name).orElse(null); + Mine oldMine = finder.find(name).orElse(null); repository.reload(name); if (oldMine != null) { - Mine newMine = repository.find(name).orElse(null); + Mine newMine = finder.find(name).orElse(null); if (newMine != null) { worldGuardService.updateRegion(name, newMine.getConfig()); } } } + public void updateMineConfig(String name, MineConfig newConfig) throws IOException { + if (!repository.exists(name)) { + throw new IllegalArgumentException("Mine not found: " + name); + } + + try { + fileManager.saveConfig(name, newConfig); + } catch (ConfigLoader.ConfigLoadException e) { + throw new IOException("Failed to save mine config: " + e.getMessage(), e); + } + + try { + Mine oldMine = finder.find(name).orElse(null); + repository.reload(name); + + if (oldMine != null) { + Mine newMine = finder.find(name).orElse(null); + if (newMine != null) { + worldGuardService.updateRegion(name, newMine.getConfig()); + } + } + } catch (ConfigLoader.ConfigLoadException e) { + throw new IOException("Failed to reload mine after config update: " + e.getMessage(), e); + } + } + public WorldGuardRegionService getWorldGuardService() { return worldGuardService; } diff --git a/src/main/java/dev/loki/lomines/core/mine/MineLoader.java b/src/main/java/dev/loki/lomines/core/mine/service/MineLoader.java similarity index 70% rename from src/main/java/dev/loki/lomines/core/mine/MineLoader.java rename to src/main/java/dev/loki/lomines/core/mine/service/MineLoader.java index 2646503..f69f6e1 100644 --- a/src/main/java/dev/loki/lomines/core/mine/MineLoader.java +++ b/src/main/java/dev/loki/lomines/core/mine/service/MineLoader.java @@ -1,10 +1,10 @@ -package dev.loki.lomines.core.common; +package dev.loki.lomines.core.mine.service; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.Mine; import dev.loki.lomines.core.service.MineFileManager; -import dev.loki.lomines.data.config.loader.common.ConfigLoader; -import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.core.service.MineRepository; +import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.data.config.model.MineConfig; import java.io.IOException; import java.nio.file.Files; @@ -14,19 +14,19 @@ import java.util.stream.Stream; /** * Loads mine configurations from disk. */ -final class MineLoader { +public final class MineLoader { private final LoMinesPlugin plugin; private final MineFileManager fileManager; private final MineRepository repository; - MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) { + public MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) { this.plugin = plugin; this.fileManager = fileManager; this.repository = repository; } - void loadAll() throws IOException { + public void loadAll() throws IOException { repository.clear(); Path minesFolder = fileManager.getMinesFolder(); @@ -49,9 +49,9 @@ final class MineLoader { MineConfig config = fileManager.loadConfig(mineName); repository.createAndStart(mineName, config); - plugin.loLogger().info("Loaded mine: " + mineName); + plugin.getLogger().info("Loaded mine: " + mineName); } catch (IOException | ConfigLoader.ConfigLoadException e) { - plugin.loLogger().error("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage()); + plugin.getLogger().severe("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage()); } } } diff --git a/src/main/java/dev/loki/lomines/core/mine/MineTicker.java b/src/main/java/dev/loki/lomines/core/mine/service/MineTicker.java similarity index 76% rename from src/main/java/dev/loki/lomines/core/mine/MineTicker.java rename to src/main/java/dev/loki/lomines/core/mine/service/MineTicker.java index 59dc3b5..bb10629 100644 --- a/src/main/java/dev/loki/lomines/core/mine/MineTicker.java +++ b/src/main/java/dev/loki/lomines/core/mine/service/MineTicker.java @@ -1,8 +1,10 @@ -package dev.loki.lomines.core.mine; +package dev.loki.lomines.core.mine.service; import dev.loki.lomines.LoMinesPlugin; -import dev.lolib.scheduler.ScheduledTask; -import dev.lolib.scheduler.Scheduler; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.core.mine.registry.Mines; +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; /** * Centralized ticker for all mines. @@ -13,7 +15,7 @@ public final class MineTicker { private final Mines mines; private final LoMinesPlugin plugin; - private ScheduledTask tickerTask; + private BukkitTask tickerTask; public MineTicker(Mines mines, LoMinesPlugin plugin) { this.mines = mines; @@ -29,7 +31,7 @@ public final class MineTicker { return; // Already running } - tickerTask = Scheduler.get(plugin).runTimer(() -> { + tickerTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { for (Mine mine : mines.getAll()) { tickMine(mine); } @@ -52,7 +54,7 @@ public final class MineTicker { private void tickMine(Mine mine) { int currentTicks = mine.getTicksAtomic().incrementAndGet(); - if (currentTicks >= mine.getConfig().getResetTicks()) { + if (currentTicks >= mine.getConfig().reset().intervalTicks()) { mine.getTicksAtomic().set(0); mine.reset(false); } diff --git a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java index 3b77c63..bfe1fbc 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java +++ b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java @@ -1,38 +1,25 @@ package dev.loki.lomines.core.service; -import dev.loki.lomines.data.config.loader.common.ConfigLoader; -import dev.loki.lomines.data.config.MineConfig; -import dev.loki.lomines.data.config.block.BlockConfig; +import dev.loki.lomines.core.service.write.MineConfigWriter; +import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.data.config.model.MineConfig; import dev.loki.lomines.data.config.block.BlockKey; -import dev.loki.lomines.data.config.block.FillMode; -import dev.loki.lomines.data.config.region.RegionConfig; -import dev.loki.lomines.data.config.reset.ResetConfig; -import dev.loki.lomines.data.config.reward.RewardConfig; -import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; -import dev.loki.lomines.data.config.teleport.TeleportConfig; -import dev.loki.lomines.data.config.ui.UIConfig; -import dev.loki.lomines.integration.worldguard.WorldGuardConfig; -import dev.loki.lomines.util.location.Cuboid; -import dev.loki.lomines.util.location.LocationParser; import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -/** - * Handles file operations for mine configurations. - * Updated for new section-based configuration (v2). - */ -public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { +public final class MineFileManager { + + private final Path minesFolder; + private final ConfigLoader configLoader; + private final MineConfigWriter configWriter; public MineFileManager(Path minesFolder) { - this(minesFolder, new ConfigLoader(minesFolder.getParent())); + this.minesFolder = minesFolder; + this.configLoader = new ConfigLoader(minesFolder.getParent()); + this.configWriter = new MineConfigWriter(minesFolder, configLoader); } public Path getMinesFolder() { @@ -45,82 +32,9 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { } } - /** - * Creates a default mine configuration with new format. - * WorldGuard region settings are loaded from defaults.yml. - */ public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException { ensureFolderExists(); - - // Create default region - Location loc1 = corner1 != null ? corner1 : new Location( - org.bukkit.Bukkit.getWorlds().get(0), 0, 64, 0); - Location loc2 = corner2 != null ? corner2 : new Location( - org.bukkit.Bukkit.getWorlds().get(0), 10, 74, 10); - - Cuboid cuboid = new Cuboid(loc1, loc2); - RegionConfig region = RegionConfig.single(cuboid); - - // Create default blocks (stone only) - Map weights = new HashMap<>(); - weights.put(new BlockKey.Vanilla(Material.STONE), 1.0); - BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); - - // Load WorldGuard defaults from _defaults.yml - WorldGuardConfig wgConfig = loadWorldGuardDefaults(); - - // Build mine config with defaults - MineConfig config = MineConfig.builder(name) - .region(region) - .blocks(blocks) - .reset(ResetConfig.defaults()) - .rewards(RewardConfig.empty()) - .teleport(TeleportConfig.disabled()) - .ui(UIConfig.defaults()) - .worldGuard(wgConfig) - .playerSpawn(PlayerSpawnConfig.disabled()) - .build(); - - // Save using new loader - configLoader.save(config); - } - - /** - * Loads WorldGuard configuration from defaults.yml. - */ - private WorldGuardConfig loadWorldGuardDefaults() { - Path defaultsPath = minesFolder.resolve("_defaults.yml"); - if (!Files.exists(defaultsPath)) { - return WorldGuardConfig.disabled(); - } - - try { - YamlConfiguration yaml = YamlConfiguration.loadConfiguration(defaultsPath.toFile()); - ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard"); - - if (wgSection == null) { - return WorldGuardConfig.disabled(); - } - - boolean enabled = wgSection.getBoolean("enabled", false); - if (!enabled) { - return WorldGuardConfig.disabled(); - } - - String template = wgSection.getString("region-template", "{mine_name}_{random_4}"); - - return WorldGuardConfig.builder() - .enabled(true) - .template(template) - .owners(wgSection.getStringList("owners")) - .members(wgSection.getStringList("members")) - .flags(wgSection.getStringList("flags")) - .protectOnCreate(wgSection.getBoolean("protect-on-create", true)) - .build(); - - } catch (Exception e) { - return WorldGuardConfig.disabled(); - } + configWriter.createDefaultConfig(name, corner1, corner2); } public MineConfig loadConfig(String name) throws IOException, ConfigLoader.ConfigLoadException { @@ -134,48 +48,16 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { } } - /** - * Saves a mine configuration to file. - * - * @param config the configuration to save - * @throws ConfigLoader.ConfigLoadException if saving fails - */ public void saveConfig(MineConfig config) throws ConfigLoader.ConfigLoadException { configLoader.save(config); } - /** - * Saves mask positions for mask fill mode. - */ - public void saveMaskPositions(String name, BlockKey markerMaterial, java.util.List positions) throws IOException, ConfigLoader.ConfigLoadException { - MineConfig config = loadConfig(name); + public void saveConfig(String name, MineConfig config) throws ConfigLoader.ConfigLoadException { + configLoader.save(config); + } - // Build new mask config - Map posMap = new HashMap<>(); - for (Location loc : positions) { - posMap.put(LocationParser.format(loc), true); - } - BlockConfig.MaskConfig mask = new BlockConfig.MaskConfig(markerMaterial, posMap); - - // Create new block config with mask - BlockConfig newBlocks = new BlockConfig( - config.blocks().weights(), - FillMode.MASK, - mask - ); - - // Build updated config - MineConfig updated = MineConfig.builder(name) - .region(config.region()) - .blocks(newBlocks) - .reset(config.reset()) - .rewards(config.rewards()) - .teleport(config.teleport()) - .ui(config.ui()) - .worldGuard(config.worldGuard()) - .playerSpawn(config.playerSpawn()) - .build(); - - configLoader.save(updated); + public void saveMaskPositions(String name, BlockKey markerMaterial, + java.util.List positions) throws IOException, ConfigLoader.ConfigLoadException { + configWriter.saveMaskPositions(name, markerMaterial, positions); } } diff --git a/src/main/java/dev/loki/lomines/core/service/MineRepository.java b/src/main/java/dev/loki/lomines/core/service/MineRepository.java index 28c8224..862377f 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineRepository.java +++ b/src/main/java/dev/loki/lomines/core/service/MineRepository.java @@ -1,13 +1,17 @@ package dev.loki.lomines.core.service; import dev.loki.lomines.LoMinesPlugin; -import dev.loki.lomines.core.mine.Mine; -import dev.loki.lomines.data.config.loader.common.ConfigLoader; -import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.data.config.model.MineConfig; import org.bukkit.Location; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; /** @@ -84,6 +88,6 @@ public final class MineRepository { stopAndRemove(name); MineConfig config = fileManager.loadConfig(name); Mine mine = createAndStart(name, config); - plugin.loLogger().info("Reloaded mine: " + name); + plugin.getLogger().info("Reloaded mine: " + name); } } diff --git a/src/main/java/dev/loki/lomines/core/service/MaskScanService.java b/src/main/java/dev/loki/lomines/core/service/mask/MaskScanService.java similarity index 83% rename from src/main/java/dev/loki/lomines/core/service/MaskScanService.java rename to src/main/java/dev/loki/lomines/core/service/mask/MaskScanService.java index 9c4e7a5..bbce989 100644 --- a/src/main/java/dev/loki/lomines/core/service/MaskScanService.java +++ b/src/main/java/dev/loki/lomines/core/service/mask/MaskScanService.java @@ -1,10 +1,12 @@ -package dev.loki.lomines.core.service; +package dev.loki.lomines.core.service.mask; -import dev.loki.lomines.core.mine.Mine; -import dev.loki.lomines.data.config.loader.common.ConfigLoader; +import dev.loki.lomines.core.mine.model.Mine; +import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.core.service.MineFileManager; +import dev.loki.lomines.core.service.MineRepository; import dev.loki.lomines.data.config.block.BlockKey; import dev.loki.lomines.util.ValidationUtils; -import dev.loki.lomines.util.location.Cuboid; +import dev.loki.lomines.util.location.geo.Cuboid; import dev.loki.lomines.util.selection.MaskScanner; import org.bukkit.Location; import org.bukkit.Material; @@ -57,8 +59,8 @@ public final class MaskScanService { } private Material parseMarkerMaterial(BlockKey key) { - if (key instanceof BlockKey.Vanilla vanilla) { - return vanilla.material(); + if (key instanceof BlockKey.Vanilla(Material material)) { + return material; } // For custom blocks (Oraxen/ItemsAdder), we need to get the underlying material // This would need integration with those plugins to get the actual material diff --git a/src/main/java/dev/loki/lomines/core/service/write/MineConfigWriter.java b/src/main/java/dev/loki/lomines/core/service/write/MineConfigWriter.java new file mode 100644 index 0000000..a683563 --- /dev/null +++ b/src/main/java/dev/loki/lomines/core/service/write/MineConfigWriter.java @@ -0,0 +1,129 @@ +package dev.loki.lomines.core.service.write; + +import dev.loki.lomines.data.config.ConfigLoader; +import dev.loki.lomines.data.config.model.MineConfig; +import dev.loki.lomines.data.config.block.BlockConfig; +import dev.loki.lomines.data.config.block.BlockKey; +import dev.loki.lomines.data.config.block.FillMode; +import dev.loki.lomines.data.config.region.RegionConfig; +import dev.loki.lomines.data.config.reset.ResetConfig; +import dev.loki.lomines.data.config.reward.RewardConfig; +import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; +import dev.loki.lomines.data.config.teleport.TeleportConfig; +import dev.loki.lomines.data.config.ui.UIConfig; +import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig; +import dev.loki.lomines.util.location.geo.Cuboid; +import dev.loki.lomines.util.location.LocationParser; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +public final class MineConfigWriter { + + private final Path minesFolder; + private final ConfigLoader configLoader; + + public MineConfigWriter(Path minesFolder, ConfigLoader configLoader) { + this.minesFolder = minesFolder; + this.configLoader = configLoader; + } + + public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException { + Location loc1 = corner1 != null ? corner1 : new Location( + org.bukkit.Bukkit.getWorlds().get(0), 0, 64, 0); + Location loc2 = corner2 != null ? corner2 : new Location( + org.bukkit.Bukkit.getWorlds().get(0), 10, 74, 10); + + Cuboid cuboid = new Cuboid(loc1, loc2); + RegionConfig region = RegionConfig.single(cuboid); + + Map weights = new HashMap<>(); + weights.put(new BlockKey.Vanilla(Material.STONE), 1.0); + BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null); + + WorldGuardConfig wgConfig = loadWorldGuardDefaults(); + + MineConfig config = MineConfig.builder(name) + .region(region) + .blocks(blocks) + .reset(ResetConfig.defaults()) + .rewards(RewardConfig.empty()) + .teleport(TeleportConfig.disabled()) + .ui(UIConfig.defaults()) + .worldGuard(wgConfig) + .playerSpawn(PlayerSpawnConfig.disabled()) + .build(); + + try { + configLoader.save(config); + } catch (ConfigLoader.ConfigLoadException e) { + throw new IOException("Failed to save default mine config: " + e.getMessage(), e); + } + } + + public void saveMaskPositions(String name, BlockKey markerMaterial, + java.util.List positions) throws IOException, ConfigLoader.ConfigLoadException { + MineConfig config = configLoader.load(name); + + Map posMap = new HashMap<>(); + for (Location loc : positions) { + posMap.put(LocationParser.format(loc), true); + } + BlockConfig.MaskConfig mask = new BlockConfig.MaskConfig(markerMaterial, posMap); + + BlockConfig newBlocks = new BlockConfig( + config.blocks().weights(), + FillMode.MASK, + mask + ); + + MineConfig updated = MineConfig.builder(name) + .region(config.region()) + .blocks(newBlocks) + .reset(config.reset()) + .rewards(config.rewards()) + .teleport(config.teleport()) + .ui(config.ui()) + .worldGuard(config.worldGuard()) + .playerSpawn(config.playerSpawn()) + .build(); + + configLoader.save(updated); + } + + private WorldGuardConfig loadWorldGuardDefaults() { + Path defaultsPath = minesFolder.resolve("_defaults.yml"); + if (!Files.exists(defaultsPath)) { + return WorldGuardConfig.disabled(); + } + try { + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(defaultsPath.toFile()); + ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard"); + if (wgSection == null) { + return WorldGuardConfig.disabled(); + } + boolean enabled = wgSection.getBoolean("enabled", false); + if (!enabled) { + return WorldGuardConfig.disabled(); + } + String template = wgSection.getString("region-template", "{mine_name}_{random_4}"); + return WorldGuardConfig.builder() + .enabled(true) + .template(template) + .owners(wgSection.getStringList("owners")) + .members(wgSection.getStringList("members")) + .flags(wgSection.getStringList("flags")) + .protectOnCreate(wgSection.getBoolean("protect-on-create", true)) + .build(); + } catch (Exception e) { + return WorldGuardConfig.disabled(); + } + } +} diff --git a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java index c0e32ff..fb41bae 100644 --- a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java @@ -5,27 +5,21 @@ import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader; import dev.loki.lomines.data.config.loader.entity.TeleportConfigLoader; import dev.loki.lomines.data.config.loader.region.RegionConfigLoader; import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader; -import dev.loki.lomines.data.config.loader.reset.ResetConfigLoader; import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader; +import dev.loki.lomines.data.config.loader.system.ResetConfigLoader; import dev.loki.lomines.data.config.loader.system.UIConfigLoader; +import dev.loki.lomines.data.config.model.MineConfig; import org.bukkit.configuration.file.YamlConfiguration; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -/** - * Clean config loader for section-based configuration. - * Supports YAML inheritance from defaults. - * Delegates to specialized loaders for each section. - */ public final class ConfigLoader { private final Path dataFolder; - private YamlConfiguration defaults; + private final DefaultsMerger defaultsMerger; - // Section loaders private RegionConfigLoader regionLoader; private BlockConfigLoader blockLoader; private ResetConfigLoader resetLoader; @@ -37,69 +31,29 @@ public final class ConfigLoader { public ConfigLoader(Path dataFolder) { this.dataFolder = dataFolder; - loadDefaults(); + this.defaultsMerger = new DefaultsMerger(dataFolder); initLoaders(); } - private void loadDefaults() { - Path defaultsPath = dataFolder.resolve("mines").resolve("_defaults.yml"); - defaults = new YamlConfiguration(); - - if (Files.exists(defaultsPath)) { - defaults = YamlConfiguration.loadConfiguration(defaultsPath.toFile()); - } else { - setDefaults(defaults); - try { - Files.createDirectories(defaultsPath.getParent()); - defaults.save(defaultsPath.toFile()); - } catch (IOException e) { - // Ignore, use in-memory defaults - } - } - } - - private void setDefaults(YamlConfiguration yaml) { - yaml.set("reset.interval", "5m"); - yaml.set("reset.percent-trigger", 10.0); - yaml.set("reset.percent-enabled", false); - yaml.set("reset.commands", List.of()); - yaml.set("reset.broadcast", ""); - - yaml.set("ui.actionbar.enabled", true); - yaml.set("ui.actionbar.format", "{mine} {percent}% ({time})"); - yaml.set("ui.actionbar.range", 50.0); - yaml.set("ui.timer-format", "mm:ss"); - - yaml.set("teleport.enabled", false); - yaml.set("rewards", List.of()); - - worldGuardLoader.setDefaults(yaml); - playerSpawnLoader.setDefaults(yaml); - } - private void initLoaders() { regionLoader = new RegionConfigLoader(); blockLoader = new BlockConfigLoader(); - resetLoader = new ResetConfigLoader(defaults); + resetLoader = new ResetConfigLoader(defaultsMerger.getDefaults()); rewardLoader = new RewardConfigLoader(); teleportLoader = new TeleportConfigLoader(); - uiLoader = new UIConfigLoader(defaults); - worldGuardLoader = new WorldGuardConfigLoader(defaults); + uiLoader = new UIConfigLoader(defaultsMerger.getDefaults()); + worldGuardLoader = new WorldGuardConfigLoader(defaultsMerger.getDefaults()); playerSpawnLoader = new PlayerSpawnConfigLoader(); } - /** - * Load a mine configuration from file. - */ public MineConfig load(String mineName) throws ConfigLoadException { - Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml"); - - if (!Files.exists(configPath)) { + if (!Files.exists(dataFolder.resolve("mines").resolve(mineName + ".yml"))) { throw new ConfigLoadException("Mine not found: " + mineName); } + Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml"); YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configPath.toFile()); - mergeDefaults(yaml); + defaultsMerger.mergeDefaults(yaml); try { return MineConfig.builder(mineName) @@ -117,11 +71,7 @@ public final class ConfigLoader { } } - /** - * Save a mine configuration to file. - */ public void save(MineConfig config) throws ConfigLoadException { - Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml"); YamlConfiguration yaml = new YamlConfiguration(); regionLoader.save(yaml, config.region()); @@ -134,6 +84,7 @@ public final class ConfigLoader { playerSpawnLoader.save(yaml, config.playerSpawn()); try { + Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml"); Files.createDirectories(configPath.getParent()); yaml.save(configPath.toFile()); } catch (IOException e) { @@ -141,21 +92,11 @@ public final class ConfigLoader { } } - private void mergeDefaults(YamlConfiguration yaml) { - for (String key : defaults.getKeys(true)) { - if (!yaml.contains(key)) { - yaml.set(key, defaults.get(key)); - } - } - } - - /** - * Exception for config loading errors. - */ public static class ConfigLoadException extends Exception { public ConfigLoadException(String message) { super(message); } + public ConfigLoadException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/dev/loki/lomines/data/config/DefaultsMerger.java b/src/main/java/dev/loki/lomines/data/config/DefaultsMerger.java new file mode 100644 index 0000000..a454c19 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/config/DefaultsMerger.java @@ -0,0 +1,71 @@ +package dev.loki.lomines.data.config; + +import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader; +import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public final class DefaultsMerger { + + private final Path dataFolder; + private YamlConfiguration defaults; + + public DefaultsMerger(Path dataFolder) { + this.dataFolder = dataFolder; + loadDefaults(); + } + + public YamlConfiguration getDefaults() { + return defaults; + } + + private void loadDefaults() { + Path defaultsPath = dataFolder.resolve("mines").resolve("_defaults.yml"); + defaults = new YamlConfiguration(); + + if (Files.exists(defaultsPath)) { + defaults = YamlConfiguration.loadConfiguration(defaultsPath.toFile()); + } else { + setDefaults(defaults); + try { + Files.createDirectories(defaultsPath.getParent()); + defaults.save(defaultsPath.toFile()); + } catch (IOException e) { + // Ignore, use in-memory defaults + } + } + } + + private void setDefaults(YamlConfiguration yaml) { + yaml.set("reset.interval", "5m"); + yaml.set("reset.percent-trigger", 10.0); + yaml.set("reset.percent-enabled", false); + yaml.set("reset.commands", List.of()); + yaml.set("reset.broadcast", ""); + + yaml.set("ui.actionbar.enabled", true); + yaml.set("ui.actionbar.format", "{mine} {percent}% ({time})"); + yaml.set("ui.actionbar.range", 50.0); + yaml.set("ui.timer-format", "mm:ss"); + + yaml.set("teleport.enabled", false); + yaml.set("rewards", List.of()); + + WorldGuardConfigLoader wgLoader = new WorldGuardConfigLoader(new YamlConfiguration()); + wgLoader.setDefaults(yaml); + PlayerSpawnConfigLoader psLoader = new PlayerSpawnConfigLoader(); + psLoader.setDefaults(yaml); + } + + public void mergeDefaults(YamlConfiguration yaml) { + for (String key : defaults.getKeys(true)) { + if (!yaml.contains(key)) { + yaml.set(key, defaults.get(key)); + } + } + } +} diff --git a/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java b/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java index fb3cbe6..2fa8a06 100644 --- a/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/block/BlockConfig.java @@ -1,5 +1,7 @@ package dev.loki.lomines.data.config.block; +import org.bukkit.Material; + import java.util.Collections; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java b/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java index eecce4e..31d552b 100644 --- a/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java +++ b/src/main/java/dev/loki/lomines/data/config/block/BlockKey.java @@ -10,12 +10,6 @@ import java.util.Objects; */ public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, BlockKey.ItemsAdder { - /** - * Serializes this key to string format for YAML storage. - * Format: "stone" (vanilla), "oraxen:my_block", "itemsadder:custom_block" - */ - String serialize(); - /** * Deserializes a string to appropriate BlockKey type. * @@ -43,6 +37,12 @@ public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, Bloc return new Vanilla(material); } + /** + * Serializes this key to string format for YAML storage. + * Format: "stone" (vanilla), "oraxen:my_block", "itemsadder:custom_block" + */ + String serialize(); + // --- Implementations --- record Vanilla(Material material) implements BlockKey { diff --git a/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/block/BlockConfigLoader.java index 352d471..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 @@ -32,7 +32,9 @@ public final class BlockConfigLoader { Map weights = new HashMap<>(); for (String key : contents.getKeys(false)) { double weight = contents.getDouble(key); - if (weight <= 0) continue; + if (weight <= 0) { + continue; + } weights.put(BlockKey.deserialize(key), weight); } @@ -40,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/RegionConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java index 409407e..b3eaf94 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/region/RegionConfigLoader.java @@ -17,32 +17,17 @@ public final class RegionConfigLoader { /** * Parses region configuration from YAML. + * Supports up to 100 selection points (50 regions). */ public RegionConfig parse(YamlConfiguration yaml) { List selections = new ArrayList<>(); - // Try new format first - for (int i = 1; i <= 10; i++) { - String key = "region.selection." + i; - if (yaml.contains(key)) { - String locStr = yaml.getString(key); - if (locStr != null && !locStr.isBlank()) { - selections.add(parseLocation(locStr)); - } - } - } + // Try new format first (region.selection.X) + selections.addAll(parseSelections(yaml, "region.selection.", 100)); - // Legacy fallback + // Legacy fallback (selection.X) if (selections.isEmpty()) { - for (int i = 1; i <= 10; i++) { - String key = "selection." + i; - if (yaml.contains(key)) { - String locStr = yaml.getString(key); - if (locStr != null && !locStr.isBlank()) { - selections.add(parseLocation(locStr)); - } - } - } + selections.addAll(parseSelections(yaml, "selection.", 100)); } if (selections.size() < 2) { @@ -52,15 +37,56 @@ public final class RegionConfigLoader { return RegionConfig.fromSelections(selections); } + /** + * Parses selection points from YAML with given prefix. + * Supports indexed format: prefix.1, prefix.2, etc. + */ + private List parseSelections(YamlConfiguration yaml, String prefix, int maxPoints) { + List selections = new ArrayList<>(); + + for (int i = 1; i <= maxPoints; i++) { + String key = prefix + i; + if (yaml.contains(key)) { + String locStr = yaml.getString(key); + if (locStr != null && !locStr.isBlank()) { + try { + selections.add(parseLocation(locStr)); + } catch (IllegalArgumentException e) { + // Skip invalid locations but continue parsing + } + } + } else { + // Stop at first missing index (assuming sequential) + // But check at least a few more for gaps + if (i > 20) { + boolean hasMore = false; + for (int j = i + 1; j <= Math.min(i + 5, maxPoints); j++) { + if (yaml.contains(prefix + j)) { + hasMore = true; + break; + } + } + if (!hasMore) { + break; + } + } + } + } + + return selections; + } + /** * Saves region configuration to YAML. */ public void save(YamlConfiguration yaml, RegionConfig region) { int i = 1; for (var cuboid : region.regions()) { - yaml.set("region.selection." + i, LocationParser.format(cuboid.getMin())); + Location min = new Location(cuboid.getWorld(), cuboid.getMinX(), cuboid.getMinY(), cuboid.getMinZ()); + yaml.set("region.selection." + i, LocationParser.format(min)); i++; - yaml.set("region.selection." + i, LocationParser.format(cuboid.getMax())); + Location max = new Location(cuboid.getWorld(), cuboid.getMaxX(), cuboid.getMaxY(), cuboid.getMaxZ()); + yaml.set("region.selection." + i, LocationParser.format(max)); i++; } } 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 021da2b..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 @@ -1,14 +1,10 @@ package dev.loki.lomines.data.config.loader.region; -import dev.loki.lomines.data.config.ConfigLoader; -import dev.loki.lomines.integration.worldguard.WorldGuardConfig; +import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; /** * Loader for WorldGuard configuration section. @@ -23,51 +19,23 @@ public final class WorldGuardConfigLoader { /** * Parses WorldGuard configuration from YAML. + * 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(); } - String template = yaml.getString("worldguard.region-template", + 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}")); - 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"); - } - - List flags = yaml.getStringList("worldguard.flags"); - if (flags.isEmpty()) { - flags = defaults.getStringList("worldguard.flags"); - } - if (flags.isEmpty()) { - flags = List.of("passthrough=deny", "build=allow"); - } - - boolean protect = yaml.getBoolean("worldguard.protect-on-create", - defaults.getBoolean("worldguard.protect-on-create", true)); - return WorldGuardConfig.builder() .enabled(true) - .template(template) + .template(tmpl) .owners(owners) .members(members) .flags(flags) @@ -75,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. */ @@ -92,13 +104,14 @@ public final class WorldGuardConfigLoader { /** * Sets default values for WorldGuard configuration. + * Default: only block-break=allow. User can add any flags. */ public void setDefaults(YamlConfiguration yaml) { yaml.set("worldguard.enabled", false); yaml.set("worldguard.region-template", "{mine_name}_{random_4}"); yaml.set("worldguard.owners", List.of()); yaml.set("worldguard.members", List.of()); - yaml.set("worldguard.flags", List.of("passthrough=deny", "build=allow")); + yaml.set("worldguard.flags", List.of("block-break=allow")); yaml.set("worldguard.protect-on-create", true); } } diff --git a/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/reward/RewardConfigLoader.java index bb4defd..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,18 +36,31 @@ public final class RewardConfigLoader { } private RewardEntry parseRewardEntry(Map map) { - double chance = ((Number) map.getOrDefault("chance", 0)).doubleValue(); + 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") @@ -55,12 +68,19 @@ public final class RewardConfigLoader { items.add(parseItemReward(itemMap)); } } + return items; + } - @SuppressWarnings("unchecked") - List commands = (List) map.getOrDefault("commands", List.of()); - boolean preventDrops = (Boolean) map.getOrDefault("prevent-drops", false); - - return new RewardEntry(blocks, chance, items, commands, preventDrops); + private List parseCommands(Object commandsObj) { + List commands = new ArrayList<>(); + if (commandsObj instanceof List list) { + for (Object obj : list) { + if (obj != null) { + commands.add(obj.toString()); + } + } + } + return commands; } private ItemReward parseItemReward(Map map) { diff --git a/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java index d939529..8667025 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/system/ResetConfigLoader.java @@ -3,8 +3,6 @@ package dev.loki.lomines.data.config.loader.system; import dev.loki.lomines.data.config.reset.ResetConfig; import org.bukkit.configuration.file.YamlConfiguration; -import java.util.List; - /** * Loader for reset configuration section. */ diff --git a/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java index d8ba26b..12cae6c 100644 --- a/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/loader/system/UIConfigLoader.java @@ -1,7 +1,9 @@ package dev.loki.lomines.data.config.loader.system; import dev.loki.lomines.data.config.ui.UIConfig; +import dev.loki.lomines.data.config.ui.HologramConfig; import org.bukkit.configuration.file.YamlConfiguration; +import java.util.List; /** * Loader for UI configuration section. @@ -25,10 +27,21 @@ public final class UIConfigLoader { actionBarEnabled, yaml.getString("ui.actionbar.format", UIConfig.DEFAULT_ACTIONBAR_FORMAT), yaml.getDouble("ui.actionbar.range", 50.0), - yaml.getString("ui.timer-format", UIConfig.DEFAULT_TIMER_FORMAT) + yaml.getString("ui.timer-format", UIConfig.DEFAULT_TIMER_FORMAT), + parseHologram(yaml) ); } + private HologramConfig parseHologram(YamlConfiguration yaml) { + boolean enabled = yaml.getBoolean("ui.hologram.enabled", false); + List format = yaml.getStringList("ui.hologram.format"); + if (format.isEmpty()) { + format = HologramConfig.defaults().format(); + } + double height = yaml.getDouble("ui.hologram.height", 2.5); + return new HologramConfig(enabled, format, height); + } + /** * Saves UI configuration to YAML. */ @@ -37,5 +50,8 @@ public final class UIConfigLoader { yaml.set("ui.actionbar.format", ui.actionBarFormat()); yaml.set("ui.actionbar.range", ui.actionBarRange()); yaml.set("ui.timer-format", ui.timerFormat()); + yaml.set("ui.hologram.enabled", ui.hologram().enabled()); + yaml.set("ui.hologram.format", ui.hologram().format()); + yaml.set("ui.hologram.height", ui.hologram().height()); } } diff --git a/src/main/java/dev/loki/lomines/data/config/model/MineConfig.java b/src/main/java/dev/loki/lomines/data/config/model/MineConfig.java new file mode 100644 index 0000000..14b1638 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/config/model/MineConfig.java @@ -0,0 +1,64 @@ +package dev.loki.lomines.data.config.model; + +import dev.loki.lomines.data.config.block.BlockConfig; +import dev.loki.lomines.data.config.region.RegionConfig; +import dev.loki.lomines.data.config.reset.ResetConfig; +import dev.loki.lomines.data.config.reward.RewardConfig; +import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; +import dev.loki.lomines.data.config.teleport.TeleportConfig; +import dev.loki.lomines.data.config.ui.UIConfig; +import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig; + +import org.bukkit.Location; + +import java.util.Objects; +import java.util.Optional; + +public record MineConfig( + String name, + RegionConfig region, + BlockConfig blocks, + ResetConfig reset, + RewardConfig rewards, + TeleportConfig teleport, + UIConfig ui, + WorldGuardConfig worldGuard, + PlayerSpawnConfig playerSpawn +) { + + public MineConfig { + Objects.requireNonNull(name, "Mine name cannot be null"); + if (name.isBlank()) { + throw new IllegalArgumentException("Mine name cannot be blank"); + } + name = name.toLowerCase().trim(); + + Objects.requireNonNull(region, "Region config cannot be null"); + Objects.requireNonNull(blocks, "Block config cannot be null"); + Objects.requireNonNull(reset, "Reset config cannot be null"); + Objects.requireNonNull(rewards, "Reward config cannot be null"); + Objects.requireNonNull(teleport, "Teleport config cannot be null"); + Objects.requireNonNull(ui, "UI config cannot be null"); + if (playerSpawn == null) { + playerSpawn = PlayerSpawnConfig.disabled(); + } + } + + public static MineConfigDefaults.Builder builder(String name) { + return new MineConfigDefaults.Builder(name); + } + + public int volume() { + return region.totalVolume(); + } + + public String worldName() { + return region.worldName(); + } + + public Optional getSpawnForStuckPlayer() { + return playerSpawn.enabled() + ? playerSpawn.getLocation() + : teleport.getLocation(); + } +} diff --git a/src/main/java/dev/loki/lomines/data/config/MineConfig.java b/src/main/java/dev/loki/lomines/data/config/model/MineConfigDefaults.java similarity index 52% rename from src/main/java/dev/loki/lomines/data/config/MineConfig.java rename to src/main/java/dev/loki/lomines/data/config/model/MineConfigDefaults.java index 2a1c8bb..593c5fa 100644 --- a/src/main/java/dev/loki/lomines/data/config/MineConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/model/MineConfigDefaults.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.config; +package dev.loki.lomines.data.config.model; import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.region.RegionConfig; @@ -7,72 +7,13 @@ import dev.loki.lomines.data.config.reward.RewardConfig; import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.data.config.ui.UIConfig; -import dev.loki.lomines.integration.worldguard.WorldGuardConfig; +import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig; -import org.bukkit.Location; +public final class MineConfigDefaults { -import java.util.Objects; -import java.util.Optional; - -/** - * Clean, type-safe, section-based mine configuration. - * Proper separation of concerns: each section handles one aspect. - */ -public record MineConfig( - String name, - RegionConfig region, - BlockConfig blocks, - ResetConfig reset, - RewardConfig rewards, - TeleportConfig teleport, - UIConfig ui, - WorldGuardConfig worldGuard, - PlayerSpawnConfig playerSpawn -) { - - public MineConfig { - Objects.requireNonNull(name, "Mine name cannot be null"); - if (name.isBlank()) { - throw new IllegalArgumentException("Mine name cannot be blank"); - } - name = name.toLowerCase().trim(); - - Objects.requireNonNull(region, "Region config cannot be null"); - Objects.requireNonNull(blocks, "Block config cannot be null"); - Objects.requireNonNull(reset, "Reset config cannot be null"); - Objects.requireNonNull(rewards, "Reward config cannot be null"); - Objects.requireNonNull(teleport, "Teleport config cannot be null"); - Objects.requireNonNull(ui, "UI config cannot be null"); - // worldGuard and playerSpawn can be null (disabled by default) - if (playerSpawn == null) { - playerSpawn = PlayerSpawnConfig.disabled(); - } + private MineConfigDefaults() { } - /** - * Total volume of the mine (for progress calculation). - */ - public int volume() { - return region.totalVolume(); - } - - /** - * World name where the mine is located. - */ - public String worldName() { - return region.worldName(); - } - - /** - * Creates builder for fluent construction. - */ - public static Builder builder(String name) { - return new Builder(name); - } - - /** - * Creates config with sensible defaults. - */ public static MineConfig defaults(String name, RegionConfig region, BlockConfig blocks) { return new MineConfig( name, @@ -87,18 +28,6 @@ public record MineConfig( ); } - /** - * Returns the spawn location for stuck players. - * If playerSpawn is not set, falls back to teleport location. - */ - public Optional getSpawnForStuckPlayer() { - return playerSpawn.enabled() - ? playerSpawn.getLocation() - : teleport.getLocation(); - } - - // --- Builder --- - public static class Builder { private final String name; private RegionConfig region; @@ -110,7 +39,7 @@ public record MineConfig( private WorldGuardConfig worldGuard = WorldGuardConfig.disabled(); private PlayerSpawnConfig playerSpawn = PlayerSpawnConfig.disabled(); - private Builder(String name) { + public Builder(String name) { this.name = name; } diff --git a/src/main/java/dev/loki/lomines/data/config/parser/ConfigParseException.java b/src/main/java/dev/loki/lomines/data/config/parser/ConfigParseException.java new file mode 100644 index 0000000..d2bf87f --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/config/parser/ConfigParseException.java @@ -0,0 +1,15 @@ +package dev.loki.lomines.data.config.parser; + +/** + * Exception for configuration parsing errors. + */ +public class ConfigParseException extends Exception { + + public ConfigParseException(String message) { + super(message); + } + + public ConfigParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java b/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java index 25dd60c..0380f97 100644 --- a/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/region/RegionConfig.java @@ -1,6 +1,6 @@ package dev.loki.lomines.data.config.region; -import dev.loki.lomines.util.location.Cuboid; +import dev.loki.lomines.util.location.geo.Cuboid; import org.bukkit.Location; import java.util.Collections; diff --git a/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java b/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java index c97ee17..bb14f83 100644 --- a/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/reset/ResetConfig.java @@ -1,13 +1,8 @@ package dev.loki.lomines.data.config.reset; import java.time.Duration; -import java.util.Collections; import java.util.List; -import java.util.Objects; -/** - * Configuration for mine reset behavior. - */ public record ResetConfig( Duration interval, double percentTrigger, @@ -16,41 +11,34 @@ public record ResetConfig( String broadcastMessage ) { - public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5); - public static final double DEFAULT_PERCENT_TRIGGER = 10.0; - public ResetConfig { - // Normalize and validate interval - if (interval == null || interval.isZero() || interval.isNegative()) { - interval = DEFAULT_INTERVAL; - } - - // Ensure interval is reasonable (1 second to 24 hours) - if (interval.getSeconds() < 1) { - interval = Duration.ofSeconds(1); - } else if (interval.getSeconds() > 86400) { - interval = Duration.ofHours(24); - } - - // Validate percent trigger - if (percentTrigger < 0 || percentTrigger > 100) { - percentTrigger = DEFAULT_PERCENT_TRIGGER; - } - + interval = ResetConfigValidator.validateInterval(interval); + percentTrigger = ResetConfigValidator.validatePercentTrigger(percentTrigger); commands = commands != null ? List.copyOf(commands) : List.of(); broadcastMessage = broadcastMessage != null ? broadcastMessage : ""; } - /** - * Interval in ticks (20 ticks = 1 second). - */ + public static final Duration DEFAULT_INTERVAL = ResetConfigValidator.DEFAULT_INTERVAL; + public static final double DEFAULT_PERCENT_TRIGGER = ResetConfigValidator.DEFAULT_PERCENT_TRIGGER; + + public static ResetConfig defaults() { + return new ResetConfig( + DEFAULT_INTERVAL, + DEFAULT_PERCENT_TRIGGER, + false, + List.of(), + "" + ); + } + + public static Builder builder() { + return new Builder(); + } + public long intervalTicks() { return interval.getSeconds() * 20; } - /** - * Human-readable interval string. - */ public String intervalDisplay() { long seconds = interval.getSeconds(); if (seconds < 60) { @@ -62,33 +50,10 @@ public record ResetConfig( } } - /** - * Whether reset should trigger based on percent mined. - */ public boolean isPercentTriggerEnabled() { return percentEnabled && percentTrigger > 0; } - /** - * Default config: 5 minutes, 10% trigger disabled. - */ - public static ResetConfig defaults() { - return new ResetConfig( - DEFAULT_INTERVAL, - DEFAULT_PERCENT_TRIGGER, - false, - List.of(), - "" - ); - } - - /** - * Builder for fluent construction. - */ - public static Builder builder() { - return new Builder(); - } - public static class Builder { private Duration interval = DEFAULT_INTERVAL; private double percentTrigger = DEFAULT_PERCENT_TRIGGER; @@ -101,13 +66,13 @@ public record ResetConfig( return this; } - public Builder intervalTicks(long ticks) { - this.interval = Duration.ofSeconds(ticks / 20); + public Builder interval(String humanReadable) { + this.interval = ResetConfigValidator.parseDuration(humanReadable); return this; } - public Builder interval(String humanReadable) { - this.interval = parseDuration(humanReadable); + public Builder intervalTicks(long ticks) { + this.interval = Duration.ofSeconds(ticks / 20); return this; } @@ -134,25 +99,5 @@ public record ResetConfig( public ResetConfig build() { return new ResetConfig(interval, percentTrigger, percentEnabled, commands, broadcastMessage); } - - private static Duration parseDuration(String s) { - s = s.trim().toLowerCase(); - try { - if (s.endsWith("s")) { - return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1))); - } else if (s.endsWith("m")) { - return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1))); - } else if (s.endsWith("h")) { - return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1))); - } else if (s.endsWith("d")) { - return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1))); - } else { - // Assume seconds if no suffix - return Duration.ofSeconds(Long.parseLong(s)); - } - } catch (NumberFormatException e) { - return DEFAULT_INTERVAL; - } - } } } diff --git a/src/main/java/dev/loki/lomines/data/config/reset/ResetConfigValidator.java b/src/main/java/dev/loki/lomines/data/config/reset/ResetConfigValidator.java new file mode 100644 index 0000000..6f3d5d4 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/config/reset/ResetConfigValidator.java @@ -0,0 +1,50 @@ +package dev.loki.lomines.data.config.reset; + +import java.time.Duration; + +public final class ResetConfigValidator { + + public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5); + public static final double DEFAULT_PERCENT_TRIGGER = 10.0; + + private ResetConfigValidator() { + } + + public static Duration validateInterval(Duration interval) { + if (interval == null || interval.isZero() || interval.isNegative()) { + interval = DEFAULT_INTERVAL; + } + if (interval.getSeconds() < 1) { + interval = Duration.ofSeconds(1); + } else if (interval.getSeconds() > 86400) { + interval = Duration.ofHours(24); + } + return interval; + } + + public static double validatePercentTrigger(double percentTrigger) { + if (percentTrigger < 0 || percentTrigger > 100) { + return DEFAULT_PERCENT_TRIGGER; + } + return percentTrigger; + } + + public static Duration parseDuration(String s) { + s = s.trim().toLowerCase(); + try { + if (s.endsWith("s")) { + return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1))); + } else if (s.endsWith("m")) { + return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1))); + } else if (s.endsWith("h")) { + return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1))); + } else if (s.endsWith("d")) { + return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1))); + } else { + return Duration.ofSeconds(Long.parseLong(s)); + } + } catch (NumberFormatException e) { + return DEFAULT_INTERVAL; + } + } +} diff --git a/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java b/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java index 9d8c2b7..1cecb4c 100644 --- a/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/reward/RewardConfig.java @@ -4,9 +4,7 @@ import dev.loki.lomines.data.config.block.BlockKey; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; /** @@ -18,6 +16,20 @@ public record RewardConfig(List entries) { entries = entries != null ? List.copyOf(entries) : List.of(); } + /** + * Empty reward config. + */ + public static RewardConfig empty() { + return new RewardConfig(List.of()); + } + + /** + * Single entry reward config builder. + */ + public static Builder builder() { + return new Builder(); + } + /** * Returns rewards applicable for the given block. */ @@ -34,20 +46,6 @@ public record RewardConfig(List entries) { return !entries.isEmpty(); } - /** - * Empty reward config. - */ - public static RewardConfig empty() { - return new RewardConfig(List.of()); - } - - /** - * Single entry reward config builder. - */ - public static Builder builder() { - return new Builder(); - } - // --- Reward Entry --- public record RewardEntry( diff --git a/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java index 8cecd00..f3ef227 100644 --- a/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java @@ -2,35 +2,36 @@ package dev.loki.lomines.data.config.spawn; import org.bukkit.Location; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.Optional; /** - * Configuration for player spawn location when stuck in blocks. + * Configuration for player spawn locations when stuck in blocks. + * Supports multiple spawn points for a single mine. * Used to teleport players out when they are suffocating after mine reset. */ -public record PlayerSpawnConfig( - boolean enabled, - Location location -) { +public record PlayerSpawnConfig(boolean enabled, List locations) { - public PlayerSpawnConfig { - if (!enabled) { - location = null; - } else if (location != null) { - // Defensive copy - location = location.clone(); - Objects.requireNonNull(location.getWorld(), "Spawn world cannot be null"); + public PlayerSpawnConfig(boolean enabled, List locations) { + this.enabled = enabled; + if (!enabled || locations == null) { + this.locations = List.of(); + } else { + // Defensive copy with validation + List copy = new ArrayList<>(); + for (Location loc : locations) { + if (loc != null) { + Location cloned = loc.clone(); + Objects.requireNonNull(cloned.getWorld(), "Spawn world cannot be null"); + copy.add(cloned); + } + } + this.locations = List.copyOf(copy); } } - /** - * Returns the spawn location if enabled. - */ - public Optional getLocation() { - return Optional.ofNullable(enabled ? location : null); - } - /** * Disabled spawn config (default). */ @@ -39,9 +40,74 @@ public record PlayerSpawnConfig( } /** - * Creates enabled config with location. + * Creates enabled config with single location. */ public static PlayerSpawnConfig at(Location location) { - return new PlayerSpawnConfig(true, location); + return new PlayerSpawnConfig(true, List.of(location)); + } + + /** + * Creates enabled config with multiple locations. + */ + public static PlayerSpawnConfig at(List locations) { + return new PlayerSpawnConfig(true, locations); + } + + /** + * Returns the primary spawn location (first in list) if enabled. + */ + public Optional getLocation() { + return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0)); + } + + /** + * Returns all spawn locations. + */ + @Override + public List locations() { + return locations; + } + + /** + * Returns true if spawn is enabled and has at least one location. + */ + @Override + public boolean enabled() { + return enabled && !locations.isEmpty(); + } + + /** + * Returns the number of configured spawn locations. + */ + public int count() { + return locations.size(); + } + + /** + * Creates a new config with an additional location. + */ + public PlayerSpawnConfig addLocation(Location location) { + List newList = new ArrayList<>(locations); + newList.add(location); + return new PlayerSpawnConfig(true, newList); + } + + /** + * Creates a new config without the location at given index. + */ + public PlayerSpawnConfig removeLocation(int index) { + if (index < 0 || index >= locations.size()) { + return this; + } + List newList = new ArrayList<>(locations); + newList.remove(index); + return new PlayerSpawnConfig(!newList.isEmpty(), newList); + } + + /** + * Clears all locations (disables spawn). + */ + public PlayerSpawnConfig clear() { + return disabled(); } } diff --git a/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java b/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java index c468b63..615b3bb 100644 --- a/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java @@ -2,34 +2,35 @@ package dev.loki.lomines.data.config.teleport; import org.bukkit.Location; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.Optional; /** * Configuration for mine teleportation on reset. + * Supports multiple teleport locations for a single mine. */ -public record TeleportConfig( - boolean enabled, - Location location -) { +public record TeleportConfig(boolean enabled, List locations) { - public TeleportConfig { - if (!enabled) { - location = null; - } else if (location != null) { - // Defensive copy - location = location.clone(); - Objects.requireNonNull(location.getWorld(), "Teleport world cannot be null"); + public TeleportConfig(boolean enabled, List locations) { + this.enabled = enabled; + if (!enabled || locations == null) { + this.locations = List.of(); + } else { + // Defensive copy with validation + List copy = new ArrayList<>(); + for (Location loc : locations) { + if (loc != null) { + Location cloned = loc.clone(); + Objects.requireNonNull(cloned.getWorld(), "Teleport world cannot be null"); + copy.add(cloned); + } + } + this.locations = List.copyOf(copy); } } - /** - * Returns the teleport location if enabled. - */ - public Optional getLocation() { - return Optional.ofNullable(enabled ? location : null); - } - /** * Disabled teleport config. */ @@ -38,9 +39,74 @@ public record TeleportConfig( } /** - * Creates enabled config with location. + * Creates enabled config with single location. */ public static TeleportConfig at(Location location) { - return new TeleportConfig(true, location); + return new TeleportConfig(true, List.of(location)); + } + + /** + * Creates enabled config with multiple locations. + */ + public static TeleportConfig at(List locations) { + return new TeleportConfig(true, locations); + } + + /** + * Returns the primary teleport location (first in list) if enabled. + */ + public Optional getLocation() { + return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0)); + } + + /** + * Returns all teleport locations. + */ + @Override + public List locations() { + return locations; + } + + /** + * Returns true if teleport is enabled and has at least one location. + */ + @Override + public boolean enabled() { + return enabled && !locations.isEmpty(); + } + + /** + * Returns the number of configured teleport locations. + */ + public int count() { + return locations.size(); + } + + /** + * Creates a new config with an additional location. + */ + public TeleportConfig addLocation(Location location) { + List newList = new ArrayList<>(locations); + newList.add(location); + return new TeleportConfig(true, newList); + } + + /** + * Creates a new config without the location at given index. + */ + public TeleportConfig removeLocation(int index) { + if (index < 0 || index >= locations.size()) { + return this; + } + List newList = new ArrayList<>(locations); + newList.remove(index); + return new TeleportConfig(!newList.isEmpty(), newList); + } + + /** + * Clears all locations (disables teleport). + */ + public TeleportConfig clear() { + return disabled(); } } diff --git a/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java b/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java index e829ef3..bdfbcd7 100644 --- a/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/ui/HologramConfig.java @@ -6,8 +6,8 @@ import java.util.List; * Configuration for holograms displayed above mines. * * @param enabled whether holograms are enabled - * @param format list of lines with placeholders {mine}, {percent}, {bar}, {time} - * @param height offset above the mine region (in blocks) + * @param format list of lines with placeholders {mine}, {percent}, {bar}, {time} + * @param height offset above the mine region (in blocks) */ public record HologramConfig( boolean enabled, diff --git a/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java b/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java index c3eb373..e4bcfc2 100644 --- a/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java @@ -1,9 +1,9 @@ package dev.loki.lomines.data.config.ui; -import net.kyori.adventure.text.minimessage.MiniMessage; +import dev.loki.lomines.util.format.color.ColorUtils; import net.kyori.adventure.text.Component; -import java.util.Objects; +import java.util.Locale; /** * Configuration for UI elements (action bar, messages). @@ -16,14 +16,11 @@ public record UIConfig( HologramConfig hologram ) { - public UIConfig { - hologram = hologram != null ? hologram : HologramConfig.disabled(); - } - public static final String DEFAULT_ACTIONBAR_FORMAT = "{mine} {percent}% ({time})"; public static final String DEFAULT_TIMER_FORMAT = "mm:ss"; public UIConfig { + hologram = hologram != null ? hologram : HologramConfig.disabled(); actionBarFormat = actionBarFormat != null && !actionBarFormat.isBlank() ? actionBarFormat : DEFAULT_ACTIONBAR_FORMAT; @@ -33,19 +30,34 @@ public record UIConfig( actionBarRange = Math.max(1, actionBarRange); } + /** + * Default configuration. + */ + public static UIConfig defaults() { + return new UIConfig(true, DEFAULT_ACTIONBAR_FORMAT, 50.0, DEFAULT_TIMER_FORMAT, HologramConfig.defaults()); + } + + /** + * Disabled configuration (no UI). + */ + public static UIConfig disabled() { + return new UIConfig(false, "", 0, DEFAULT_TIMER_FORMAT, HologramConfig.disabled()); + } + /** * Parses the action bar format with placeholders. * Placeholders: {mine}, {percent}, {time}, {blocks}, {total} + * Supports colors: &#RRGGBB, &a..&f, */ public Component formatActionBar(String mineName, double percent, String timeStr, int blocks, int total) { String parsed = actionBarFormat .replace("{mine}", mineName) - .replace("{percent}", String.format("%.1f", percent)) + .replace("{percent}", String.format(Locale.US, "%.1f", percent)) .replace("{time}", timeStr) .replace("{blocks}", String.valueOf(blocks)) .replace("{total}", String.valueOf(total)); - return MiniMessage.miniMessage().deserialize(parsed); + return ColorUtils.format(parsed); } /** @@ -70,18 +82,4 @@ public record UIConfig( public double actionBarRangeSquared() { return actionBarRange * actionBarRange; } - - /** - * Default configuration. - */ - public static UIConfig defaults() { - return new UIConfig(true, DEFAULT_ACTIONBAR_FORMAT, 50.0, DEFAULT_TIMER_FORMAT, HologramConfig.defaults()); - } - - /** - * Disabled configuration (no UI). - */ - public static UIConfig disabled() { - return new UIConfig(false, "", 0, DEFAULT_TIMER_FORMAT, HologramConfig.disabled()); - } } diff --git a/src/main/java/dev/loki/lomines/data/reward/Reward.java b/src/main/java/dev/loki/lomines/data/reward/entity/Reward.java similarity index 83% rename from src/main/java/dev/loki/lomines/data/reward/Reward.java rename to src/main/java/dev/loki/lomines/data/reward/entity/Reward.java index 9b5bed4..7b15d36 100644 --- a/src/main/java/dev/loki/lomines/data/reward/Reward.java +++ b/src/main/java/dev/loki/lomines/data/reward/entity/Reward.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward; +package dev.loki.lomines.data.reward.entity; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -49,8 +49,12 @@ public record Reward(List materials, double chance, List it @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } Reward reward = (Reward) o; return Double.compare(reward.chance, chance) == 0 && preventDrops == reward.preventDrops && @@ -59,4 +63,8 @@ public record Reward(List materials, double chance, List it Objects.equals(commands, reward.commands); } + @Override + public int hashCode() { + return Objects.hash(materials, chance, items, commands, preventDrops); + } } diff --git a/src/main/java/dev/loki/lomines/data/reward/RewardParser.java b/src/main/java/dev/loki/lomines/data/reward/parse/RewardEntryParser.java similarity index 65% rename from src/main/java/dev/loki/lomines/data/reward/RewardParser.java rename to src/main/java/dev/loki/lomines/data/reward/parse/RewardEntryParser.java index 13ef724..44ced56 100644 --- a/src/main/java/dev/loki/lomines/data/reward/RewardParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/parse/RewardEntryParser.java @@ -1,69 +1,29 @@ -package dev.loki.lomines.data.reward; +package dev.loki.lomines.data.reward.parse; import dev.loki.lomines.data.config.parser.ConfigParseException; -import org.bukkit.configuration.file.YamlConfiguration; +import dev.loki.lomines.data.reward.entity.Reward; +import dev.loki.lomines.data.reward.parse.command.RewardCommandParser; +import dev.loki.lomines.data.reward.parse.item.RewardItemParser; +import dev.loki.lomines.data.reward.parse.item.RewardMaterialParser; import org.bukkit.inventory.ItemStack; import java.util.ArrayList; import java.util.List; import java.util.Map; -/** - * Parses reward configurations from YAML. - * Handles reward chance, materials, items, commands, and prevent-drops settings. - */ -public final class RewardParser { +public final class RewardEntryParser { private final RewardItemParser itemParser; private final RewardMaterialParser materialParser; private final RewardCommandParser commandParser; - public RewardParser() { + public RewardEntryParser() { this.itemParser = new RewardItemParser(); this.materialParser = new RewardMaterialParser(); this.commandParser = new RewardCommandParser(); } - public List parseRewards(YamlConfiguration yaml) throws ConfigParseException { - List rewards = new ArrayList<>(); - - if (!yaml.contains("random-rewards")) { - return rewards; - } - - List rewardsList = yaml.getList("random-rewards"); - if (rewardsList == null) { - return rewards; - } - - for (int i = 0; i < rewardsList.size(); i++) { - Object rewardObj = rewardsList.get(i); - - if (!(rewardObj instanceof Map)) { - throw new ConfigParseException( - "Invalid reward at index " + i + ": expected map, got " + - (rewardObj != null ? rewardObj.getClass().getSimpleName() : "null") - ); - } - - @SuppressWarnings("unchecked") - Map rewardMap = (Map) rewardObj; - - try { - Reward reward = parseReward(rewardMap); - rewards.add(reward); - } catch (ConfigParseException e) { - throw new ConfigParseException( - "Error parsing reward at index " + i + ": " + e.getMessage(), - e - ); - } - } - - return rewards; - } - - private Reward parseReward(Map rewardMap) throws ConfigParseException { + public Reward parseReward(Map rewardMap) throws ConfigParseException { double chance = parseRewardField(rewardMap, "chance", Double.class); boolean preventDrops = parseRewardField(rewardMap, "prevent-drops", Boolean.class, false); @@ -75,7 +35,7 @@ public final class RewardParser { } @SuppressWarnings("unchecked") - private T parseRewardField(Map map, String key, Class type) + public T parseRewardField(Map map, String key, Class type) throws ConfigParseException { if (!map.containsKey(key)) { throw new ConfigParseException("Missing required '" + key + "' field in reward"); @@ -96,7 +56,7 @@ public final class RewardParser { } @SuppressWarnings("unchecked") - private T parseRewardField(Map map, String key, Class type, T defaultValue) { + public T parseRewardField(Map map, String key, Class type, T defaultValue) { if (!map.containsKey(key)) { return defaultValue; } @@ -107,7 +67,7 @@ public final class RewardParser { return defaultValue; } - private Double parseDoubleValue(String key, Object value) throws ConfigParseException { + public Double parseDoubleValue(String key, Object value) throws ConfigParseException { try { if (value instanceof Number) { return ((Number) value).doubleValue(); @@ -124,7 +84,7 @@ public final class RewardParser { } } - private List parseRewardItems(Map rewardMap) + public List parseRewardItems(Map rewardMap) throws ConfigParseException { List items = new ArrayList<>(); if (!rewardMap.containsKey("items")) { diff --git a/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java b/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java new file mode 100644 index 0000000..7752d2f --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/reward/parse/RewardParser.java @@ -0,0 +1,58 @@ +package dev.loki.lomines.data.reward.parse; + +import dev.loki.lomines.data.config.parser.ConfigParseException; +import dev.loki.lomines.data.reward.entity.Reward; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class RewardParser { + + private final RewardEntryParser entryParser; + + public RewardParser() { + this.entryParser = new RewardEntryParser(); + } + + public List parseRewards(YamlConfiguration yaml) throws ConfigParseException { + List rewards = new ArrayList<>(); + + if (!yaml.contains("random-rewards")) { + return rewards; + } + + List rewardsList = yaml.getList("random-rewards"); + if (rewardsList == null) { + return rewards; + } + + for (int i = 0; i < rewardsList.size(); i++) { + rewards.add(parseRewardAt(rewardsList.get(i), i)); + } + + return rewards; + } + + private Reward parseRewardAt(Object rewardObj, int i) throws ConfigParseException { + if (!(rewardObj instanceof Map)) { + throw new ConfigParseException( + "Invalid reward at index " + i + ": expected map, got " + + (rewardObj != null ? rewardObj.getClass().getSimpleName() : "null") + ); + } + + @SuppressWarnings("unchecked") + Map rewardMap = (Map) rewardObj; + + try { + return entryParser.parseReward(rewardMap); + } catch (ConfigParseException e) { + throw new ConfigParseException( + "Error parsing reward at index " + i + ": " + e.getMessage(), + e + ); + } + } +} diff --git a/src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java b/src/main/java/dev/loki/lomines/data/reward/parse/command/RewardCommandParser.java similarity index 84% rename from src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java rename to src/main/java/dev/loki/lomines/data/reward/parse/command/RewardCommandParser.java index a93ab1b..d058411 100644 --- a/src/main/java/dev/loki/lomines/data/reward/RewardCommandParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/parse/command/RewardCommandParser.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward; +package dev.loki.lomines.data.reward.parse.command; import dev.loki.lomines.data.config.parser.ConfigParseException; @@ -9,10 +9,10 @@ import java.util.Map; /** * Parses reward commands from configuration. */ -final class RewardCommandParser { +public final class RewardCommandParser { @SuppressWarnings("unchecked") - List parseCommands(Map rewardMap) throws ConfigParseException { + public List parseCommands(Map rewardMap) throws ConfigParseException { List commands = new ArrayList<>(); if (!rewardMap.containsKey("commands")) { return commands; diff --git a/src/main/java/dev/loki/lomines/data/reward/RewardItemParser.java b/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardItemParser.java similarity index 93% rename from src/main/java/dev/loki/lomines/data/reward/RewardItemParser.java rename to src/main/java/dev/loki/lomines/data/reward/parse/item/RewardItemParser.java index b52bf30..49c7d7e 100644 --- a/src/main/java/dev/loki/lomines/data/reward/RewardItemParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardItemParser.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward; +package dev.loki.lomines.data.reward.parse.item; import dev.loki.lomines.data.config.parser.ConfigParseException; import dev.loki.lomines.util.ValidationUtils; @@ -12,7 +12,7 @@ import java.util.Map; * Parses ItemStack objects from YAML reward item configurations. * Handles material type, amount, and basic item properties. */ -final class RewardItemParser { +public final class RewardItemParser { /** * Parses a single reward item from a map configuration. @@ -22,7 +22,7 @@ final class RewardItemParser { * @return the parsed ItemStack * @throws ConfigParseException if the item configuration is invalid */ - ItemStack parseItem(Map itemMap, int index) throws ConfigParseException { + public ItemStack parseItem(Map itemMap, int index) throws ConfigParseException { if (!itemMap.containsKey("type")) { throw new ConfigParseException("Missing required 'type' field in item at index " + index); } diff --git a/src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java b/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardMaterialParser.java similarity index 88% rename from src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java rename to src/main/java/dev/loki/lomines/data/reward/parse/item/RewardMaterialParser.java index 9850d04..1085935 100644 --- a/src/main/java/dev/loki/lomines/data/reward/RewardMaterialParser.java +++ b/src/main/java/dev/loki/lomines/data/reward/parse/item/RewardMaterialParser.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.reward; +package dev.loki.lomines.data.reward.parse.item; import dev.loki.lomines.data.config.parser.ConfigParseException; import dev.loki.lomines.util.ValidationUtils; @@ -11,10 +11,10 @@ import java.util.Map; /** * Parses reward materials (blocks) from configuration. */ -final class RewardMaterialParser { +public final class RewardMaterialParser { @SuppressWarnings("unchecked") - List parseMaterials(Map rewardMap) throws ConfigParseException { + public List parseMaterials(Map rewardMap) throws ConfigParseException { if (!rewardMap.containsKey("blocks")) { throw new ConfigParseException("Missing required 'blocks' field in reward"); } diff --git a/src/main/java/dev/loki/lomines/data/stats/StatsManager.java b/src/main/java/dev/loki/lomines/data/stats/StatsManager.java deleted file mode 100644 index 17a9e1e..0000000 --- a/src/main/java/dev/loki/lomines/data/stats/StatsManager.java +++ /dev/null @@ -1,181 +0,0 @@ -package dev.loki.lomines.data.stats; - -import dev.loki.lomines.LoMinesPlugin; -import dev.lolib.scheduler.ScheduledTask; -import dev.lolib.scheduler.Scheduler; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Manages player statistics for all mines. - * Handles loading, saving, and auto-saving of player statistics. - */ -public final class StatsManager { - - private final Map stats = new ConcurrentHashMap<>(); - private final LoMinesPlugin plugin; - private final Path statsFile; - private final Leaderboard leaderboard; - private ScheduledTask saveTask; - - public StatsManager(LoMinesPlugin plugin) { - this.plugin = plugin; - this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml"); - this.leaderboard = new Leaderboard(this); - } - - /** - * Loads statistics from file. - */ - public void load() throws IOException { - if (!Files.exists(statsFile)) { - plugin.loLogger().info("Stats file does not exist, starting with empty statistics"); - return; - } - - File file = statsFile.toFile(); - YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file); - - ConfigurationSection playersSection = yaml.getConfigurationSection("players"); - if (playersSection == null) { - plugin.loLogger().info("No players section found in stats.yml"); - return; - } - - for (String uuidString : playersSection.getKeys(false)) { - try { - UUID playerId = UUID.fromString(uuidString); - ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString); - - if (playerSection == null) { - continue; - } - - PlayerStats playerStats = getOrCreate(playerId); - - // Load total blocks - long totalBlocks = playerSection.getLong("total", 0); - playerStats.setTotalBlocks(totalBlocks); - - // Load mine-specific stats - ConfigurationSection minesSection = playerSection.getConfigurationSection("mines"); - if (minesSection != null) { - for (String mineName : minesSection.getKeys(false)) { - long mineBlocks = minesSection.getLong(mineName, 0); - playerStats.setMineBlocks(mineName, mineBlocks); - } - } - } catch (IllegalArgumentException e) { - plugin.loLogger().warn("Invalid UUID in stats.yml: " + uuidString); - } - } - - plugin.loLogger().info("Loaded statistics for " + stats.size() + " player(s)"); - } - - /** - * Saves statistics to file. - */ - public void save() throws IOException { - YamlConfiguration yaml = new YamlConfiguration(); - - for (Map.Entry entry : stats.entrySet()) { - UUID playerId = entry.getKey(); - PlayerStats playerStats = entry.getValue(); - - String path = "players." + playerId.toString(); - - // Save total blocks - yaml.set(path + ".total", playerStats.getTotalBlocks()); - - // Save mine-specific stats - Map mineStats = playerStats.getMineStatsSnapshot(); - if (!mineStats.isEmpty()) { - for (Map.Entry mineEntry : mineStats.entrySet()) { - yaml.set(path + ".mines." + mineEntry.getKey(), mineEntry.getValue()); - } - } - } - - // Ensure parent directory exists - if (!Files.exists(statsFile.getParent())) { - Files.createDirectories(statsFile.getParent()); - } - - yaml.save(statsFile.toFile()); - } - - /** - * Starts auto-save task. - */ - public void startAutoSave() { - // Auto-save every 5 minutes (20 ticks/second * 60 seconds * 5 minutes) - long interval = 20L * 60L * 5L; - - saveTask = Scheduler.get(plugin).runTimerAsync( - () -> { - try { - save(); - plugin.loLogger().info("Statistics auto-saved"); - } catch (IOException e) { - plugin.loLogger().error("Failed to auto-save statistics: " + e.getMessage()); - } - }, - interval, - interval - ); - } - - /** - * Stops auto-save task. - */ - public void stopAutoSave() { - if (saveTask != null) { - saveTask.cancel(); - saveTask = null; - } - } - - /** - * Gets or creates player statistics. - */ - public PlayerStats getOrCreate(UUID playerId) { - return stats.computeIfAbsent(playerId, PlayerStats::new); - } - - /** - * Increments block count for a player in a specific mine. - */ - public void incrementBlocks(UUID playerId, String mineName) { - PlayerStats playerStats = getOrCreate(playerId); - playerStats.incrementTotal(); - playerStats.incrementMine(mineName); - - // Invalidate leaderboard cache - leaderboard.invalidateCache(); - } - - /** - * Returns all player statistics. - */ - public Collection getAllStats() { - return Collections.unmodifiableCollection(stats.values()); - } - - /** - * Returns the leaderboard instance. - */ - public Leaderboard getLeaderboard() { - return leaderboard; - } -} diff --git a/src/main/java/dev/loki/lomines/data/stats/Leaderboard.java b/src/main/java/dev/loki/lomines/data/stats/model/Leaderboard.java similarity index 97% rename from src/main/java/dev/loki/lomines/data/stats/Leaderboard.java rename to src/main/java/dev/loki/lomines/data/stats/model/Leaderboard.java index 710e2b1..8d9d9e1 100644 --- a/src/main/java/dev/loki/lomines/data/stats/Leaderboard.java +++ b/src/main/java/dev/loki/lomines/data/stats/model/Leaderboard.java @@ -1,4 +1,6 @@ -package dev.loki.lomines.data.stats; +package dev.loki.lomines.data.stats.model; + +import dev.loki.lomines.data.stats.service.StatsManager; import java.util.Comparator; import java.util.List; diff --git a/src/main/java/dev/loki/lomines/data/stats/LeaderboardEntry.java b/src/main/java/dev/loki/lomines/data/stats/model/LeaderboardEntry.java similarity index 71% rename from src/main/java/dev/loki/lomines/data/stats/LeaderboardEntry.java rename to src/main/java/dev/loki/lomines/data/stats/model/LeaderboardEntry.java index 810ebba..f6f0cb3 100644 --- a/src/main/java/dev/loki/lomines/data/stats/LeaderboardEntry.java +++ b/src/main/java/dev/loki/lomines/data/stats/model/LeaderboardEntry.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.stats; +package dev.loki.lomines.data.stats.model; import java.util.Objects; import java.util.UUID; @@ -16,12 +16,21 @@ public record LeaderboardEntry(UUID playerId, long count) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } LeaderboardEntry that = (LeaderboardEntry) o; return count == that.count && playerId.equals(that.playerId); } + @Override + public int hashCode() { + return Objects.hash(playerId, count); + } + @Override public String toString() { return "LeaderboardEntry{playerId=" + playerId + ", count=" + count + "}"; diff --git a/src/main/java/dev/loki/lomines/data/stats/PlayerStats.java b/src/main/java/dev/loki/lomines/data/stats/model/PlayerStats.java similarity index 97% rename from src/main/java/dev/loki/lomines/data/stats/PlayerStats.java rename to src/main/java/dev/loki/lomines/data/stats/model/PlayerStats.java index 91f1e78..2bd8f5d 100644 --- a/src/main/java/dev/loki/lomines/data/stats/PlayerStats.java +++ b/src/main/java/dev/loki/lomines/data/stats/model/PlayerStats.java @@ -1,4 +1,4 @@ -package dev.loki.lomines.data.stats; +package dev.loki.lomines.data.stats.model; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/dev/loki/lomines/data/stats/service/StatsManager.java b/src/main/java/dev/loki/lomines/data/stats/service/StatsManager.java new file mode 100644 index 0000000..bed2f78 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/stats/service/StatsManager.java @@ -0,0 +1,72 @@ +package dev.loki.lomines.data.stats.service; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.data.stats.model.Leaderboard; +import dev.loki.lomines.data.stats.model.PlayerStats; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class StatsManager { + + private final Map stats = new ConcurrentHashMap<>(); + private final LoMinesPlugin plugin; + private final Leaderboard leaderboard; + private final StatsPersistence persistence; + + public StatsManager(LoMinesPlugin plugin) { + this.plugin = plugin; + this.leaderboard = new Leaderboard(this); + this.persistence = new StatsPersistence(plugin, this); + } + + public StatsPersistence getPersistence() { + return persistence; + } + + public void load() throws IOException { + persistence.load(stats); + } + + public void save() throws IOException { + persistence.save(stats); + } + + public void startAutoSave() { + persistence.startAutoSave(() -> { + try { + save(); + plugin.getLogger().info("Statistics auto-saved"); + } catch (IOException e) { + plugin.getLogger().severe("Failed to auto-save statistics: " + e.getMessage()); + } + }); + } + + public void stopAutoSave() { + persistence.stopAutoSave(); + } + + public PlayerStats getOrCreate(UUID playerId) { + return stats.computeIfAbsent(playerId, PlayerStats::new); + } + + public void incrementBlocks(UUID playerId, String mineName) { + PlayerStats playerStats = getOrCreate(playerId); + playerStats.incrementTotal(); + playerStats.incrementMine(mineName); + leaderboard.invalidateCache(); + } + + public Collection getAllStats() { + return Collections.unmodifiableCollection(stats.values()); + } + + public Leaderboard getLeaderboard() { + return leaderboard; + } +} diff --git a/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java b/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java new file mode 100644 index 0000000..936c000 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/stats/service/StatsPersistence.java @@ -0,0 +1,114 @@ +package dev.loki.lomines.data.stats.service; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.data.stats.model.PlayerStats; +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.UUID; + +public final class StatsPersistence { + + private final LoMinesPlugin plugin; + private final Path statsFile; + private final StatsManager statsManager; + private BukkitTask saveTask; + + public StatsPersistence(LoMinesPlugin plugin, StatsManager statsManager) { + this.plugin = plugin; + this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml"); + this.statsManager = statsManager; + } + + public void load(Map stats) throws IOException { + if (!Files.exists(statsFile)) { + plugin.getLogger().info("Stats file does not exist, starting with empty statistics"); + return; + } + + File file = statsFile.toFile(); + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file); + + ConfigurationSection playersSection = yaml.getConfigurationSection("players"); + if (playersSection == null) { + plugin.getLogger().info("No players section found in stats.yml"); + return; + } + + for (String uuidString : playersSection.getKeys(false)) { + loadPlayer(stats, uuidString, playersSection); + } + + plugin.getLogger().info("Loaded statistics for " + stats.size() + " player(s)"); + } + + private void loadPlayer(Map stats, String uuidString, ConfigurationSection playersSection) { + try { + UUID playerId = UUID.fromString(uuidString); + ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString); + + if (playerSection == null) { + return; + } + + PlayerStats playerStats = stats.computeIfAbsent(playerId, PlayerStats::new); + + long totalBlocks = playerSection.getLong("total", 0); + playerStats.setTotalBlocks(totalBlocks); + + ConfigurationSection minesSection = playerSection.getConfigurationSection("mines"); + if (minesSection != null) { + for (String mineName : minesSection.getKeys(false)) { + long mineBlocks = minesSection.getLong(mineName, 0); + playerStats.setMineBlocks(mineName, mineBlocks); + } + } + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Invalid UUID in stats.yml: " + uuidString); + } + } + + public void save(Map stats) throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + + for (Map.Entry entry : stats.entrySet()) { + UUID playerId = entry.getKey(); + PlayerStats playerStats = entry.getValue(); + + String path = "players." + playerId.toString(); + yaml.set(path + ".total", playerStats.getTotalBlocks()); + + Map mineStats = playerStats.getMineStatsSnapshot(); + if (!mineStats.isEmpty()) { + for (Map.Entry mineEntry : mineStats.entrySet()) { + yaml.set(path + ".mines." + mineEntry.getKey(), mineEntry.getValue()); + } + } + } + + if (!Files.exists(statsFile.getParent())) { + Files.createDirectories(statsFile.getParent()); + } + + yaml.save(statsFile.toFile()); + } + + public void startAutoSave(Runnable saveAction) { + long interval = 20L * 60L * 5L; + saveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, saveAction, interval, interval); + } + + public void stopAutoSave() { + if (saveTask != null) { + saveTask.cancel(); + saveTask = null; + } + } +} diff --git a/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java b/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java index 352d383..f29a415 100644 --- a/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java +++ b/src/main/java/dev/loki/lomines/gui/common/ItemStackFactory.java @@ -13,11 +13,12 @@ import java.util.List; /** * Factory for creating ItemStacks with name and lore. */ -final class ItemStackFactory { +public final class ItemStackFactory { - private ItemStackFactory() {} + private ItemStackFactory() { + } - static ItemStack create(Material mat, String name, String... loreLines) { + public static ItemStack create(Material mat, String name, String... loreLines) { ItemStack stack = new ItemStack(mat); ItemMeta meta = stack.getItemMeta(); if (meta != null) { @@ -32,17 +33,21 @@ final class ItemStackFactory { return stack; } - static String loc(Location l) { - if (l == null || l.getWorld() == null) return "§7не задана"; + public static String loc(Location l) { + if (l == null || l.getWorld() == null) { + return "§7не задана"; + } return String.format("§7%.0f§8/§7%.0f§8/§7%.0f", l.getX(), l.getY(), l.getZ()); } - static String trunc(String s, int max) { - if (s == null || s.isEmpty()) return "§7нет"; + public static String trunc(String s, int max) { + if (s == null || s.isEmpty()) { + return "§7нет"; + } return s.length() > max ? s.substring(0, max - 3) + "..." : s; } - static ItemStack filler() { + public static ItemStack filler() { ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemMeta meta = pane.getItemMeta(); if (meta != null) { diff --git a/src/main/java/dev/loki/lomines/gui/common/package-info.java b/src/main/java/dev/loki/lomines/gui/common/package-info.java deleted file mode 100644 index 5f05b16..0000000 --- a/src/main/java/dev/loki/lomines/gui/common/package-info.java +++ /dev/null @@ -1,13 +0,0 @@ -/** - * GUI editors for mine configuration. - *

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

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

Reset process:

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

Updated for new configuration system (v2).

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

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

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

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

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

This class is thread-safe due to its immutability.

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