LoMines/ARCHITECTURE.md

24 KiB
Raw Blame History

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<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.