# ARCHITECTURE.md — LoMines (Java, Paper) > **Лицензия:** GNU General Public License v3.0 > Исходный код распространяется на условиях GPL-3. Любые производные работы обязаны публиковаться под той же лицензией. > Полный текст: https://www.gnu.org/licenses/gpl-3.0.html ## Обзор LoMines — Minecraft-плагин системы шахт для Paper 1.19.2+ (сборка на API 1.21.4, Java 21). Плагин написан на чистом Java без сторонних minecraft-фреймворков и реализует: - Управление шахтами (создание, удаление, сброс, перезагрузка, конфигурация) - Мультирегиональные шахты (до 5 пар точек → 5 `Cuboid`-регионов) - Два режима заполнения: `CUBOID` (весь регион) и `MASK` (только по меткам) - Взвешенные блоки (шансы нормализуются автоматически) - Авто-сбросы: по интервалу (таймер) или по проценту добытых блоков - WorldGuard-интеграцию (авто-создание/обновление/удаление регионов) - GUI-редактор шахт (main/confirm/group) - Статистику игроков и лидерборды (кэшируемые) - PlaceholderAPI-интеграцию - Голограммы (DecentHolograms / HolographicDisplays — провайдеры) - Групповую палку для массового создания шахт, wand-частицы --- ## Зависимости | Зависимость | Scope | Откуда / Зачем | |---|---|---| | `paper-api:1.21.4-R0.1-SNAPSHOT` | compileOnly | Paper API | | `commons-math3:3.6.1` | implementation | `EnumeratedDistribution` — взвешенный рандом | | `commons-io:2.18.0` | implementation | утилиты для работы с файлами | | `placeholderapi:2.11.6` | compileOnly | PlaceholderAPI | | `worldguard-bukkit:7.0.13` | compileOnly | WorldGuard | | `worldedit-bukkit:7.3.10` | compileOnly | WorldEdit (нужен для WG API) | | `junit-jupiter:5.11.4` | test | JUnit 5 | | `kotest-*:5.9.1` | test | Property-based тесты и ассерты Kotest | | `mockito-core / mockito-inline` | test | Моки для тестов | | `adventure-api / minimessage` | test | MiniMessage в тестах | > Сборка: Gradle + shadow (`build/libs/LoMines-3.0.0.jar`). Проверка качества: Checkstyle (`config/checkstyle/checkstyle.xml`), severity=error. --- ## Структура пакетов ``` dev.loki.lomines/ │ ├── LoMinesPlugin.java # главный класс, extends JavaPlugin ├── ComponentInitializer.java # создание компонентов, загрузка шахт, тикер, статистика ├── RegistrationManager.java # регистрация команд, слушателей, интеграций │ ├── 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/ │ ├── model/ # PlayerStats, Leaderboard, LeaderboardEntry │ └── service/ # StatsManager, StatsPersistence │ ├── block/ │ ├── BlockSetter.java # абстрактный setter (fill / fillAtLocations) │ ├── BukkitBlockSetter.java # vanilla через Bukkit API (async fill) │ ├── OraxenBlockSetter.java.disabled # отключено (внешние плагины выпилены) │ └── ItemsAdderBlockSetter.java.disabled │ ├── handler/ │ ├── reset/ # MineResetHandler, PlayerTeleportHandler │ ├── block/ # MineBlockHandler │ ├── reward/ # MineRewardHandler │ └── ui/ # ActionBarHandler │ ├── listener/ │ ├── block/ # BlockBreakListener │ ├── gui/ # MineEditGuiListener, GuiActionHandler, GroupGuiListener │ └── player/ # PlayerInteractListener, PlayerJoinListener │ ├── 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/) │ ├── 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 ``` --- ## Жизненный цикл плагина ``` 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.onDisable() ├── mineTicker.stop() ├── mines.getAll().forEach(Mine::stop) # отмена actionbar task ├── statsManager.stopAutoSave() + save() # stats.yml ├── integrationManager.shutdown() # unregister PAPI ├── wandParticleService.stopAll() └── hologramManager.shutdown() ``` Все компоненты создаются в `ComponentInitializer` и прокидываются через конструкторы — инъекция зависимостей вручную, без DI-фреймворка. --- ## Ключевые классы ### `Mine` (`core/mine/model/Mine.java`) Координатор одной шахты. Бизнес-логика делегирована в handler-ы: | Метод | Делегирует | |---|---| | `reset(silent)` | `MineResetHandler.reset` | | `onBlockBreak(player, block)` | `MineBlockHandler.handle` | | `contains(location)` / `appliesToBlock(location)` | `RegionConfig` / `MineState` | | `start()` / `stop()` | actionbar task | Поля: `name`, `regions` (List), `config` (MineConfig), `blockSetter`, `state` (MineState), `resetHandler`, `blockHandler`, `actionBarHandler`, `maskBlockKeys`. ### `MineState` (`core/mine/model/MineState.java`) Потокобезопасное состояние шахты: - `AtomicInteger blocks`, `AtomicInteger ticks` - `totalVolume` — для MASK = число позиций маски, для CUBOID = объём регионов - `getPercentFilled()` = `blocks / totalVolume * 100` ### `Mines` (`core/mine/registry/Mines.java`) Фасад над реестром и сервисами: `MineFileManager` (файлы), `MineRepository` (in-memory), `MineLoader` (загрузка), `MaskScanService`, `WorldGuardRegionService`, `MineFinder` (поиск). Методы: `loadAll`, `create`, `delete`, `get/find/getAll/findByLocation`, `reloadMine`, `updateMineConfig`, `scanAndSaveMask`. ### `MineRepository` (`core/service/MineRepository.java`) `ConcurrentHashMap` (ключ — имя в lowercase). `createAndStart(name, config)` → `new Mine` + `add` + `start`. `reload` = stop → loadConfig → createAndStart. ### `MineFileManager` (`core/service/MineFileManager.java`) Файловые операции в `plugins/LoMines/mines/`: `ensureFolderExists`, `createDefaultConfig`, `loadConfig`, `saveConfig`, `deleteConfig`, `saveMaskPositions`. Оборачивает `ConfigLoader` + `MineConfigWriter`. ### `MineTicker` (`core/mine/service/MineTicker.java`) Один общий `BukkitTask` (1L, 1L) тикает все шахты. Если `ticks >= reset.intervalTicks()` → сброс и обнуление счётчика. ### `MineResetHandler` (`handler/reset/MineResetHandler.java`) Потокобезопасный сброс: ``` reset(silent) ├── running.compareAndSet(false, true) # защита от двойного сброса ├── MASK → blockSetter.fillAtLocations(maskPositions, cb) ├── CUBOID → для каждого Cuboid: blockSetter.fill(region, cb) │ когда все регионы завершены (AtomicInteger completed): │ Bukkit.runTask(plugin) → onResetComplete(placed, silent) └── onResetComplete: blocks.set(placed); ticks.set(0) executeResetCommands() # %mine% → имя шахты broadcastReset() # MiniMessage teleportStuckPlayers() # если teleport.enabled running.set(false) ``` ### `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) └── 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) ``` --- ## Система статистики ``` 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 (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` — форматирование строк. --- ## Принципы **SOLID:** - `Mine` — координатор без логики, делегирует в handler-ы (SRP) - `BlockSetter` — новые реализации добавляются наследованием (OCP) - Все `BlockSetter` взаимозаменяемы в `MineResetHandler` (LSP) - Handler-классы зависят только от нужного (ISP) - `Mine` зависит от абстракции `BlockSetter`, не от конкретной реализации (DIP) **Ограничения на код:** - Максимум 150 строк на файл, 30 строк на метод (план) — фактически проверяется Checkstyle (см. `config/checkstyle/checkstyle.xml`) - Асинхронность — только через `Bukkit.getScheduler()`, никогда `new Thread()` напрямую - Bukkit API — только в main thread (кроме async-fill и async-save статистики) - Внутренние зависимости — конструкторы, без static-синглтонов --- ## Тесты 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`.