385 lines
24 KiB
Markdown
385 lines
24 KiB
Markdown
# 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`.
|