LoMines/ARCHITECTURE.md

385 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<Cuboid>), `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<String, Mine>` (ключ — имя в 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<Location>, 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/<name>.yml` | Конфигурация отдельной шахты |
### `MineConfig` (record)
```
MineConfig(
name, String
region, RegionConfig (List<Cuboid> regions, volume, contains)
blocks, BlockConfig (Map<BlockKey, Double> 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<Mine>
└── 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<UUID, PlayerStats> stats # ConcurrentHashMap
├── load() / save() → StatsPersistence # stats.yml (players.<uuid>.total / .mines.<name>)
├── 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<String, AtomicLong> 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>_<attr>%` (name/blocks/total/percent/percentint/world/remaining/resettime/resetseconds), `%lomines_player_<attr>%` (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`.