LoMines/ARCHITECTURE.md
loki5512344 bd32b9fab2 chore: initial commit - LoMines v3.0.0 base structure
- Java rewrite from Kotlin
- LoAPI 3.0.0 integration
- Paper 1.21 support
- Core mine system with handlers
- BlockSetter abstraction (Bukkit/Oraxen/ItemsAdder)
- Statistics and leaderboard system
- Group wand for batch mine creation
- Mask fill mode support

Known issues:
- ConfigLoader.java: 712 lines (needs refactoring)
- MineCommands.java: 341 lines (needs splitting)
- 7 classes exceed 200 line limit
- Code quality improvements needed (KISS, DRY, SOLID)
2026-04-23 23:01:11 +02:00

771 lines
31 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 Rewrite)
> **Лицензия:** GNU General Public License v3.0
> Исходный код распространяется на условиях GPL-3. Любые производные работы обязаны публиковаться под той же лицензией.
> Полный текст: https://www.gnu.org/licenses/gpl-3.0.html
## Обзор
LoMines — Minecraft-плагин системы шахт, основанный на Paper API.
Данный документ описывает архитектуру **Java-версии** (переписана с Kotlin).
В качестве базовой библиотеки используется **LoAPI (lolib 2.0.1)**.
Плагин реализует:
- Управление шахтами (создание, удаление, сброс, конфигурация)
- Мультирегиональные шахты (до 10 `Cuboid`-регионов, по 5 пар точек)
- Быструю/стандартную установку блоков с поддержкой Oraxen и ItemsAdder
- Систему наград при добыче блоков
- Статистику игроков и лидерборды
- Интеграции с PlaceholderAPI и WorldGuard
- GUI-редактор шахт
- Визуализацию выделения частицами
---
## Зависимости
| Зависимость | Scope | Откуда / Зачем |
|---|---|---|
| `paper-api:1.21-R0.1-SNAPSHOT` | compileOnly | Paper API |
| `lolib-2.0.1.jar` | implementation (shadow) | LoAPI — базовая библиотека (scheduler, commands, gui, items, utils, config, metrics, placeholders, performance) |
| `placeholderapi:2.11.6` | compileOnly | PlaceholderAPI |
| `worldguard-bukkit:7.0.9` | compileOnly | WorldGuard |
| `oraxen:1.161.0` | compileOnly | Oraxen custom blocks |
| `api-itemsadder:3.6.1` | compileOnly | ItemsAdder custom blocks |
| `commons-math3:3.6.1` | compileOnly | `EnumeratedDistribution` — взвешенный рандом для блоков и наград |
| `commons-io:2.17.0` | compileOnly | `FileUtils.listFiles()` для сканирования папки mines/ |
| `junit-jupiter:5.11.3` | testImplementation | JUnit тесты |
| `kotest-runner-junit5:5.9.1` | testImplementation | Property-based тесты |
> LoAPI шейдится в JAR плагина. Relocate: `dev.lolib` → `com.loki.lomines.libs.lolib`
---
## Структура пакетов
```
com.loki.lomines/
│
├── LoMinesPlugin.java # extends LoPlugin (LoAPI)
│
├── commands/
│ ├── LoMinesCommand.java # @Command("lm") — корневая команда /lm
│ ├── admin/
│ │ ├── MineManagementCommands.java # create, delete, redefine
│ │ ├── MineControlCommands.java # reset, reload, list
│ │ └── DebugCommands.java # testtime, checktime
│ ├── player/
│ │ ├── MineNavigationCommands.java # wand, tp, settp
│ │ ├── MineEditorCommands.java # editor
│ │ └── NotificationCommands.java # messages, notify
│ ├── region/
│ │ └── WorldGuardCommands.java # region create/delete/process
│ └── stats/
│ └── StatsCommands.java # stats, top, stats reset
│
├── config/
│ └── impl/
│ ├── Config.java # config.yml — глобальные настройки
│ ├── Messages.java # messages.yml — локализация
│ └── MineConfig.java # mines/<n>.yml — конфиг шахты
│
├── mines/
│ ├── Mine.java # Основной класс шахты
│ ├── Mines.java # Реестр всех шахт (static singleton)
│ ├── MineTicker.java # Тик всех шахт — Scheduler из LoAPI
│ │
│ ├── handlers/
│ │ ├── MineBlockHandler.java # Обработка BlockBreakEvent
│ │ ├── MineResetHandler.java # Логика сброса (fill + уведомления)
│ │ ├── MineRewardHandler.java # Выдача наград, парсинг конфига
│ │ ├── MineActionBarHandler.java # ActionBar — ActionBar из LoAPI utils
│ │ └── MineConfigLoader.java # Загрузка кубоидов, создание BlockSetter
│ │
│ ├── setter/
│ │ ├── BlockSetter.java # Абстрактный класс
│ │ ├── BukkitBlockSetter.java # Стандартный Bukkit (setBlockData)
│ │ ├── OraxenBlockSetter.java # Oraxen custom blocks
│ │ └── ItemsAdderBlockSetter.java # ItemsAdder custom blocks
│ │
│ └── editor/
│ ├── base/
│ │ ├── BaseEditor.java # Базовый GUI — InventoryGUI из LoAPI
│ │ ├── BasePaginatedEditor.java # Пагинация — PagedGUI из LoAPI
│ │ └── EditorUtils.java
│ ├── MinesEditor.java
│ ├── MineEditor.java
│ ├── MineContentsEditor.java
│ ├── MineRewardsEditor.java
│ ├── MineRewardEditor.java
│ └── reward/
│ ├── RewardCommandsEditor.java
│ ├── RewardItemsEditor.java
│ └── RewardBlocksEditor.java
│
├── stats/
│ ├── PlayerStats.java # Данные одного игрока (AtomicLong, ConcurrentHashMap)
│ ├── StatsManager.java # Центральный менеджер статистики
│ ├── StatsStorage.java # Чтение/запись stats.yml
│ └── Leaderboard.java # Топы по total и по шахтам
│
├── selection/
│ ├── Selection.java # До 5 пар точек (pos1..pos10), currentPair
│ ├── SelectionWand.java # Инструмент выделения + частицы
│ └── SelectionPairGui.java # GUI выбора пары — InventoryGUI из LoAPI
│
├── integrations/
│ ├── PlaceholderAPIIntegration.java # extends PlaceholderExpansion
│ ├── WorldGuardIntegration.java
│ └── WGRegionManager.java
│
├── listener/
│ └── BlockListener.java # BlockBreakEvent, PlayerInteractEvent, PlayerJoinEvent
│
├── converters/
│ └── CataMinesConverter.java # Конвертация из CataMines
│
└── utils/
├── ChunkUtils.java # sendMultiBlockChange / sendBlockChange по чанкам
├── FileUtils.java # Пути к папке плагина, копирование ресурсов
├── NotificationManager.java # Отключение уведомлений — ConcurrentHashSet + yaml
└── TimeUtils.java # Форматирование времени — TimeFormatter из LoAPI
```
---
## Использование LoAPI
### Главный класс — `LoPlugin`
`LoMinesPlugin` наследует `LoPlugin` вместо `JavaPlugin`.
```java
public class LoMinesPlugin extends LoPlugin {
public static LoMinesPlugin INSTANCE;
@Override
protected void enable() {
INSTANCE = this;
loLogger().info("LoMines включается...");
// Определение активных интеграций
SimpleFeatureFlags features = SimpleFeatureFlags.create()
.enable("placeholderapi", Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null)
.enable("worldguard", Bukkit.getPluginManager().getPlugin("WorldGuard") != null)
.enable("oraxen", Bukkit.getPluginManager().getPlugin("Oraxen") != null)
.enable("itemsadder", Bukkit.getPluginManager().getPlugin("ItemsAdder") != null)
.enable("statistics", Config.STATISTICS_ENABLED);
// Команды через CommandManager из LoAPI
CommandManager commands = new CommandManager(this);
commands.registerTabCompleter("mine", ctx -> new ArrayList<>(Mines.getTypes().keySet()));
commands.register(new LoMinesCommand());
commands.register(new MineManagementCommands());
commands.register(new MineControlCommands());
commands.register(new DebugCommands());
commands.register(new MineNavigationCommands());
commands.register(new MineEditorCommands());
commands.register(new NotificationCommands());
commands.register(new WorldGuardCommands());
commands.register(new StatsCommands());
Bukkit.getPluginManager().registerEvents(new BlockListener(), this);
features.ifEnabled("placeholderapi", () -> new PlaceholderAPIIntegration().register());
features.ifEnabled("worldguard", this::initWorldGuard);
// Метрики через LoAPI Metrics
features.ifEnabled("statistics", () -> {
Metrics metrics = new Metrics(this, 20058);
metrics.addCustomChart(new Metrics.SimplePie("mines_count",
() -> String.valueOf(Mines.getTypes().size())));
});
MineTicker.schedule(this);
SelectionWand.startParticleTask(this);
NotificationManager.init();
StatsManager.load();
scheduleStatsSave();
reload();
loLogger().info("LoMines v" + getDescription().getVersion() + " включен!");
}
@Override
protected void disable() {
SelectionWand.stopParticleTask();
StatsManager.save();
Scheduler.get(this).cancelAll();
loLogger().info("LoMines выключен!");
}
@Override
protected void dependencies(DependencyManager manager) {
// Runtime-загрузка зависимостей которых нет в LoAPI
manager.add("org.apache.commons", "commons-math3", "3.6.1");
manager.add("commons-io", "commons-io", "2.17.0");
}
public void reload() {
Config.reload();
Mines.reload();
}
}
```
---
### Scheduler
`MineTicker` и все задержанные/асинхронные операции используют `Scheduler` из LoAPI.
Работает на Paper и Folia без изменений кода.
```java
// MineTicker.java — тик всех шахт
public class MineTicker {
public static void schedule(Plugin plugin) {
Scheduler.get(plugin).runTimer(() ->
Mines.getTypes().forEach((name, mine) -> mine.tick()), 1L, 1L);
}
}
// Частицы выделения каждые 10 тиков
Scheduler.get(plugin).runTimer(SelectionWand::tickParticles, 0L, 10L);
// Автосохранение статистики (async, не блокирует main thread)
Scheduler.get(this).runTimerAsync(() -> StatsManager.save(), intervalTicks, intervalTicks);
// Sync-коллбэк после завершения fill (вызывается из callback BlockSetter)
Scheduler.get(plugin).run(() -> {
executeResetCommands();
sendResetNotifications(silent);
teleportPlayers();
running.set(false);
});
// Обновление чанков с задержкой 2 тика
Scheduler.get(plugin).runLater(() -> ChunkUtils.refreshChunksForPlayers(cuboid), 2L);
```
---
### Команды — `CommandManager`
Все команды используют аннотации LoAPI: `@Command`, `@Subcommand`, `@Arg`, `@Cooldown`, `@PlayerOnly`, `@Permission`.
```java
@Command(value = "lm", aliases = {"lomines"})
public class LoMinesCommand {
@Default
public void onHelp(CommandSender sender) { /* help */ }
@Subcommand(value = "reload", permission = "lomines.command.reload")
public void onReload(CommandSender sender) {
LoMinesPlugin.INSTANCE.reload();
sender.sendMessage(Messages.RELOAD);
}
@Subcommand(value = "list", permission = "lomines.command.list")
public void onList(CommandSender sender) {
Mines.getTypes().forEach((name, mine) -> sender.sendMessage("- " + name));
}
}
@Command(value = "lm")
public class MineControlCommands {
@Subcommand(value = "reset", permission = "lomines.command.reset")
public void onReset(CommandSender sender,
@Arg(value = "mine", completer = "mine") String mineName) {
Mine mine = Mines.valueOf(mineName);
mine.reset(false);
}
}
@Command(value = "lm")
public class MineNavigationCommands {
@Subcommand(value = "tp", permission = "lomines.command.teleport")
@PlayerOnly
@Cooldown(seconds = 3)
public void onTp(Player player,
@Arg(value = "mine", completer = "mine") String mineName) {
Mine mine = Mines.valueOf(mineName);
player.teleport(Serializers.LOCATION.deserialize(mine.config.TELEPORT_LOCATION));
}
@Subcommand(value = "wand", permission = "lomines.command.wand")
@PlayerOnly
public void onWand(Player player) {
player.getInventory().addItem(SelectionWand.getWand());
}
}
@Command(value = "lm")
public class StatsCommands {
@Subcommand(value = "stats", permission = "lomines.command.stats")
@PlayerOnly
public void onStats(Player player) {
PlayerStats stats = StatsManager.getStats(player);
player.sendMessage("Всего сломано: " + stats.getTotalBlocks());
}
@Subcommand(value = "top", permission = "lomines.command.top")
public void onTop(CommandSender sender) {
Leaderboard.getTopTotal(10).forEach(entry ->
sender.sendMessage(entry.position() + ". " + entry.playerName() + " — " + entry.blocks()));
}
}
```
---
### GUI — `InventoryGUI`
Редакторы шахт используют `InventoryGUI` из LoAPI.
```java
// BaseEditor.java
public abstract class BaseEditor {
protected final Player player;
protected final InventoryGUI gui; // dev.lolib.gui.InventoryGUI
public BaseEditor(Player player, String title, int rows) {
this.player = player;
this.gui = new InventoryGUI(LoMinesPlugin.INSTANCE, Component.text(title), rows);
}
protected void setItem(int slot, ItemStack item, Consumer<Player> onClick) {
gui.setItem(slot, item, onClick::accept);
}
public void open() {
buildItems();
gui.open(player);
}
protected abstract void buildItems();
}
// MineContentsEditor.java
public class MineContentsEditor extends BaseEditor {
public MineContentsEditor(Player player, Mine mine) {
super(player, "Блоки: " + mine.name, 6);
}
@Override
protected void buildItems() {
setItem(49, ItemBuilder.of(Material.LIME_DYE).name("<green>Добавить").build(),
p -> openAddBlockMenu(p));
// ... слоты с блоками
}
}
```
---
### ItemBuilder
```java
import dev.lolib.items.ItemBuilder;
// Кнопка сброса
ItemStack btn = ItemBuilder.of(Material.REDSTONE)
.name("<red>Сбросить шахту")
.lore("<gray>Заполнение: <white>" + percent + "%")
.build();
// Заглушка
ItemStack filler = ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
// Палочка выделения
ItemStack wand = ItemBuilder.of(Material.GOLDEN_AXE)
.name("<#00AAFF><b>Палочка выделения")
.lore("<#00AAFF><b>ЛКМ <gray>— позиция #1")
.lore("<#00AAFF><b>ПКМ <gray>— позиция #2")
.lore("<#00AAFF><b>Shift+ПКМ <gray>— выбор региона")
.persistentData(wandKey, PersistentDataType.BYTE, (byte) 0)
.build();
```
---
### Логирование — `LoLogger`
```java
// Вместо getLogger() везде используем loLogger()
private static final LoLogger LOGGER = LoMinesPlugin.INSTANCE.loLogger();
LOGGER.info("Шахта " + name + " создана, объём: " + volume);
LOGGER.warn("Мир не найден для шахты " + name);
LOGGER.error("Не удалось создать BlockSetter", e);
if (Config.DEBUG) LOGGER.debug("Сброс занял " + took + "мс");
```
---
### Форматирование — `StringUtils` / `TimeFormatter`
```java
import dev.lolib.utils.StringUtils;
import dev.lolib.utils.TimeFormatter;
// MiniMessage строки с плейсхолдерами
String msg = StringUtils.format(mine.config.ACTION_BAR,
"notbroken", String.valueOf(blocks),
"total", String.valueOf(mine.volume),
"percent", String.format("%.2f", blocks / mine.volume * 100.0),
"blocksbroken", String.format("%.2f", mine.volume - blocks),
"time", formatTime(remainingMs, mine.config.TIMER_FORMAT)
);
// Форматирование времени по TIMER_FORMAT
private String formatTime(long ms, int format) {
Duration d = Duration.ofMillis(ms);
return switch (format) {
case 1 -> TimeFormatter.formatDigital(d); // "01:25:35"
case 2 -> TimeFormatter.formatShort(d); // "1ч 25м"
default -> TimeFormatter.formatSmart(d); // "01ч 25м 35с"
};
}
```
---
## Ключевые классы
### `Mine.java`
Координирует handler-ы. Никакой логики — только делегирование.
```java
public class Mine {
public final String name;
public final MineConfig config;
public final List<Cuboid> cuboids; // до 5 кубоидов
private final MineBlockHandler blockHandler;
private final MineResetHandler resetHandler;
private final MineRewardHandler rewardHandler;
private final MineActionBarHandler actionBarHandler;
private final MineConfigLoader configLoader;
private BlockSetter placer;
public double volume; // суммарный объём всех кубоидов
public double blocks; // текущее кол-во неломаных блоков
private long tick; // тик с последнего сброса
public void tick() { ... }
public void onBlockBreak(Player player, Block block, BlockBreakEvent event) { ... }
public void reset(boolean silent) { ... }
public void reload(boolean reset) { ... }
public boolean containsLocation(Location loc) { ... } // any cuboid
public double getDistanceToMine(Location loc) { ... } // min dist to any cuboid
public World getWorld() { ... } // world of first cuboid
}
```
### `Mines.java`
```java
public final class Mines {
private static final Map<String, Mine> TYPES = new HashMap<>();
// ключ = mine.name.toLowerCase()
public static void reload() { ... }
public static void register(Mine mine) { ... }
public static void unregister(Mine mine) { ... } // удаляет + файл конфига
public static Mine valueOf(String name) { ... } // throws IllegalArgumentException
public static Map<String, Mine> getTypes() { ... }
}
```
### `BlockSetter.java`
```java
public abstract class BlockSetter {
protected final World world;
protected final EnumeratedDistribution<?> distribution; // commons-math3
public abstract void fill(Cuboid cuboid, IntConsumer consumer);
// consumer.accept(blocksPlaced) — вызывается после завершения заполнения
}
```
Выбор реализации в `MineConfigLoader.createBlockSetter()`:
- ключ в `contents` содержит `oraxen:` → `OraxenBlockSetter`
- ключ содержит `itemsadder:` → `ItemsAdderBlockSetter`
- иначе → `BukkitBlockSetter` (vanilla)
### `MineResetHandler.java`
```java
public class MineResetHandler {
private final AtomicBoolean running = new AtomicBoolean(false);
public void reset(boolean silent, BlockSetter placer) {
if (!running.compareAndSet(false, true)) return; // защита от двойного сброса
// для каждого cuboid: placer.fill(cub, callback)
// когда ВСЕ кубоиды завершены (AtomicInteger completedCuboids):
// Scheduler.get(plugin).run(() -> постобработка + running.set(false))
}
}
```
### `MineActionBarHandler.java`
Каждые 10 тиков отправляет action bar игрокам в радиусе `ACTION_BAR_RANGE²`.
```java
// Отправка через ActionBar из LoAPI utils
import dev.lolib.utils.ActionBar;
ActionBar.send(player, StringUtils.format(mine.config.ACTION_BAR,
"notbroken", ..., "total", ..., "percent", ..., "time", ...));
```
### `PlayerStats.java`
```java
public class PlayerStats {
public final UUID uuid;
public String playerName;
private final AtomicLong totalBlocks = new AtomicLong(0);
private final ConcurrentHashMap<String, AtomicLong> mineStats = new ConcurrentHashMap<>();
// ключ = mineName.toLowerCase()
public void incrementTotal() { ... }
public void incrementMine(String mineName) { ... } // computeIfAbsent → new AtomicLong
public long getMineStat(String mineName) { ... }
public void resetMine(String mineName) { ... } // вычитает из total, мин 0
public void setTotalBlocks(long count) { ... } // только для десериализации
public void setMineStat(String mineName, long count) { ... }
}
```
### `Selection.java`
Хранит до 5 пар точек. Ключ в `Map<Integer, Location>` = `(pair-1)*2 + (first?1:2)`.
```
pair=1: pos[1], pos[2]
pair=2: pos[3], pos[4]
...
pair=5: pos[9], pos[10]
currentPair — активная пара (переключается через SelectionPairGui)
```
---
## Поток данных при разрушении блока
```
BlockBreakEvent (MONITOR, ignoreCancelled=true)
└── BlockListener.onBlockBreakEvent()
└── for mine : Mines.getTypes().values()
if mine.containsLocation(block.location):
├── mine.onBlockBreak(player, block, event)
│ ├── MineBlockHandler: blocks--
│ ├── MineRewardHandler.getRandomRewards(block)
│ │ └── rand <= chance/100 → reward.execute(player)
│ │ ├── dispatchCommand (от консоли)
│ │ └── player.inventory.addItem(items)
│ └── if blocks/volume*100 <= RESET_PERCENT
│ && Config.RESET_ON_PERCENT_ENABLED → mine.reset()
│
└── StatsManager.incrementStats(player, mine)
└── playerStats.incrementTotal() + incrementMine(mine.name)
```
---
## Поток сброса шахты
```
mine.reset(silent)
├── tick = 0
├── actionBarHandler.reset()
├── if resetHandler.isRunning() → return (защита от двойного сброса)
├── mine.blocks = mine.volume (немедленно, до завершения fill)
└── resetHandler.reset(silent, placer)
├── running.compareAndSet(false, true)
├── for each cuboid:
│ placer.fill(cuboid, placed -> {
│ if (++completed >= total):
│ Scheduler.get(plugin).run(() -> {
│ executeResetCommands()
│ sendResetNotifications(silent)
│ teleportPlayers()
│ running.set(false)
│ })
│ })
└── после fill → ChunkUtils.refreshChunksDelayed(cuboid, 2L)
```
---
## Поток загрузки плагина
```
LoMinesPlugin.enable() [extends LoPlugin]
├── LoPlugin.onLoad() → dependencies(manager) // commons-math3, commons-io
├── loLogger().info(...)
├── SimpleFeatureFlags → определение активных интеграций
├── CommandManager(this)
│ .registerTabCompleter("mine", ...)
│ .register(9 классов команд)
├── Bukkit.registerEvents(new BlockListener(), this)
├── features.ifEnabled("placeholderapi") → register()
├── features.ifEnabled("statistics") → new Metrics(this, 20058)
├── MineTicker.schedule(this) // Scheduler.runTimer(1L, 1L)
├── SelectionWand.startParticleTask(this) // Scheduler.runTimer(0L, 10L)
├── NotificationManager.init()
├── StatsManager.load()
├── scheduleStatsSave() // Scheduler.runTimerAsync(...)
└── reload()
├── Config.reload()
└── Mines.reload()
```
---
## Жизненный цикл Mine-объекта
```
new Mine(file, reset=true)
├── Mines.register(this)
└── reload(reset=true)
├── config.reload()
├── cuboids = configLoader.loadCuboids()
├── volume = configLoader.calculateVolume(cuboids)
├── blocks = volume
├── rewardHandler.loadRewards()
├── placer = configLoader.createBlockSetter(world)
└── if reset: mine.reset()
Mine.tick() [каждый тик]
├── tick++
├── if tick >= RESET_TICKS → reset(false); tick = 0
└── actionBarHandler.tick(tick, blocks)
Mines.unregister(mine)
├── TYPES.remove(mine.name)
└── mine.config.file.delete()
```
---
## Конфигурация шахты (MineConfig)
Файл: `plugins/LoMines/mines/<n>.yml`
| Поле | Ключ в yaml | Тип | Описание |
|---|---|---|---|
| `DISPLAY_NAME` | `display-name` | String | MiniMessage имя |
| `CONTENTS` | `contents` | Map | блок → вес (нормализует EnumeratedDistribution) |
| `SELECTION_CORNER_1..10` | `selection.1..10` | String | `"world;x;y;z;yaw;pitch"` |
| `RESET_TICKS` | `reset.ticks` | long | тики до автосброса (20 = 1 сек) |
| `RESET_PERCENT` | `reset.percent` | double | % для автосброса по проценту |
| `BROADCAST_RESET` | `broadcast-reset` | int | -2=откл, -1=все, 0=мир, N=радиус |
| `TELEPORT_ON_RESET` | `teleport-on-reset` | int | 0=верхний блок, 1=settp, иное=откл |
| `TELEPORT_LOCATION` | `teleport-location` | String | `"world;x;y;z;yaw;pitch"` |
| `ACTION_BAR_ENABLED` | `actionbar.enabled` | boolean | показывать action bar |
| `ACTION_BAR_RANGE` | `actionbar.range` | int | радиус (проверяется как range²) |
| `TIMER_FORMAT` | `timer-format` | int | 1=digital, 2=short, 3=full |
| ~~`SETTER`~~ | ~~`setter`~~ | ~~String~~ | ~~Удалено (только Bukkit API)~~ |
| `RANDOM_REWARDS` | `random-rewards` | List\<Map\> | список наград |
| `RESET_COMMANDS` | `reset-commands` | List\<String\> | команды после сброса |
### Структура блока награды
```yaml
- chance: 0.01
prevent-drops: false
blocks:
- "diamond_ore"
items:
- type: diamond
amount: 1
name: "<gold>Редкий алмаз"
commands:
- "eco give <player> 500"
```
---
## Интеграции
### PlaceholderAPI
| Плейсхолдер | Возвращает |
|---|---|
| `%lomines_percent_<mine>%` | `blocks/volume*100` (2 знака) |
| `%lomines_notbroken_<mine>%` | `mine.blocks` |
| `%lomines_blocksbroken_<mine>%` | `volume - blocks` |
| `%lomines_total_<mine>%` | `mine.volume` |
| `%lomines_resettime_<mine>%` | время до сброса |
| `%lomines_stats_total%` | `playerStats.totalBlocks` |
| `%lomines_stats_<mine>%` | `playerStats.getMineStat(mine)` |
| `%lomines_top_<pos>_name%` | имя в общем топе |
| `%lomines_top_<pos>_count%` | блоков в общем топе |
| `%lomines_top_<mine>_<pos>_name%` | имя в топе по шахте |
| `%lomines_top_<mine>_<pos>_count%` | блоков в топе по шахте |
### WorldGuard
`WGRegionManager` создаёт/удаляет WG-регион при операциях с шахтой.
Настройки: `Config.WORLDGUARD_ENABLED`, `WORLDGUARD_AUTO_CREATE_REGIONS`, `WORLDGUARD_REGION_PRIORITY`.
---
## Система статистики
```
StatsManager
├── ConcurrentHashMap<UUID, PlayerStats> stats
├── load() ← enable()
├── save() ← по таймеру Scheduler.runTimerAsync + disable()
└── incrementStats(Player, Mine)
├── if !Config.STATISTICS_ENABLED → return
└── stats.computeIfAbsent(uuid) → incrementTotal() + incrementMine(mine.name)
Leaderboard (stateless)
├── getTopTotal(limit) → сортировка по totalBlocks desc
├── getTopByMine(mine, limit) → фильтр + сортировка по getMineStat(mine) desc
├── getPosition(uuid) → 1-based, -1 если нет
└── getPositionByMine(uuid, mine)
```
---
## Принципы
**SOLID:**
- `Mine` — координатор без логики, делегирует в handler-ы (SRP)
- `BlockSetter` — новые реализации добавляются наследованием (OCP)
- Все `BlockSetter` взаимозаменяемы в `MineResetHandler` (LSP)
- Handler-классы зависят только от нужного (ISP)
- `Mine` зависит от абстракции `BlockSetter`, не от конкретного NMS (DIP)
**Ограничения на код:**
- Максимум 200 строк на файл (кроме сгенерированных/библиотечных)
- Асинхронность — только через `Scheduler.get(plugin)`, никогда `new Thread()`
- Bukkit API — только в main thread (кроме async-save статистики)
- Изменяемое static состояние — только в `Mines.TYPES` и `StatsManager.stats`
---
## Известные проблемы
| # | Проблема | Файл | Приоритет |
|---|---|---|---|
| 1 | `Leaderboard.getTopTotal()` — O(n log n) без кэша | `Leaderboard.java` | средний |
| 2 | `mine.blocks = mine.volume` выставляется до завершения fill — ложный процент-reset во время сброса | `Mine.java` | средний |
| 3 | `MineResetHandler.reset()` принимает `BlockSetter` параметром — лишняя связность | `MineResetHandler.java` | низкий |
| 4 | Нет тестов на `MineResetHandler`, `MineBlockHandler`, `BlockListener`, `Mines` | `test/` | высокий |
| 5 | `ChunkUtils` определяет Paper через `Class.forName` — хрупко | `ChunkUtils.java` | низкий |
| 6 | `SelectionWand.drawLine` не ограничивает частицы по Y — спам для высоких шахт | `SelectionWand.java` | низкий |