24 KiB
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 tickstotalVolume— для 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):
blocks.decrementAndGet()rewardHandler.checkRewards(player, block)- статистика:
statsManager.incrementBlocks(uuid, mineName) - если
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) — требуют PaperRegistryAccessили WorldGuard, недоступных в unit-среде. Запуск:./gradlew test.