Сначала зафиксируйте допустимые переходы
Учебная модель использует WAITING → RUNNING → FINISHING → WAITING. Новый runId появляется при старте и сохраняется у каждого таймера. Для реальной арены добавьте PREPARING/CLEANUP, когда требуется дождаться строительства или возврата игроков: эти переходы должны подтверждаться отдельным результатом.
Создайте пустую папку round-demo, установите Temurin JDK 21 и проверьте javac -version. Код ниже не зависит от Bukkit, не создаёт мир и не выдаёт награды; его задача — проверить принятие события по владельцу и состоянию.
Сохраните и запустите рабочую модель
Сохраните файл RoundDemo.java без package. В терминале этой папки выполните javac -encoding UTF-8 RoundDemo.java, затем java RoundDemo. Команды одинаковы в Windows PowerShell и Linux/macOS. Синтаксис запуска/компиляции есть в документации javac.
import java.util.UUID;
public final class RoundDemo {
enum State { WAITING, RUNNING, FINISHING }
private State state = State.WAITING;
private UUID runId;
UUID start() {
if (state != State.WAITING) throw new IllegalStateException("Раунд уже занят");
runId = UUID.randomUUID();
state = State.RUNNING;
return runId;
}
boolean finish(UUID expected) {
if (state != State.RUNNING || !expected.equals(runId)) return false;
state = State.FINISHING;
return true;
}
void cleanup(UUID expected) {
if (state != State.FINISHING || !expected.equals(runId))
throw new IllegalStateException("Не тот этап очистки");
state = State.WAITING;
runId = null;
}
public static void main(String[] args) {
RoundDemo round = new RoundDemo();
UUID first = round.start();
System.out.println("Первый финиш: " + round.finish(first));
System.out.println("Повторный финиш: " + round.finish(first));
round.cleanup(first);
UUID second = round.start();
System.out.println("Старый таймер: " + round.finish(first));
System.out.println("Новый финиш: " + round.finish(second));
round.cleanup(second);
}
}Сравните вывод с ожидаемым порядком
Должны получиться четыре результата: true, false, false, true. Первый закрывает приём результатов, второй отклоняет повтор той же сессии, третий отклоняет таймер прежнего раунда, четвёртый завершает актуальный. Между первым и новым стартом обязательно вызывается cleanup.
Для проверки запрета старта добавьте второй round.start() сразу после первого: программа должна остановиться с IllegalStateException, пока арена занята. Для поздней очистки вызов cleanup(first) после старта второго раунда тоже обязан отклоняться. Эти случаи проверяют последствия переходов, а не только enum.
Подключите модель к Paper без гонки потоков
В Paper вызывайте переходы и игровые API в основном потоке; таймер получает захваченный UUID и проверяет его перед действием. Создание/отмена задач описаны в Paper Scheduling. Async-запрос базы возвращает результат через main-задачу и сверяет тот же runId.
FINISHING сначала сохраняет результат и намерение награды, затем возвращает игрока и очищает принадлежащие сессии ресурсы. Возврат true в нашем примере ещё не является устойчивой записью: для рестарта нужен журнал. Если очистка не удалась, слот остаётся занятым и новый старт запрещён до завершения компенсации.
Опишите переходы таблицей, прежде чем писать таймеры
Для каждого состояния перечислите вход, выход и допустимые события. В WAITING можно присоединяться; в PREPARING проверяется набор участников и готовность арены; в RUNNING считается игровой результат; в FINISHING результат уже закрыт; в CLEANUP освобождаются ресурсы. Названия условные, но их смысл должен быть однозначным во всех обработчиках.
У перехода есть причина: заполнено лобби, истёк таймер, выполнена цель, погиб игрок или выключается сервер. Вместе с состоянием храните runId либо поколение. Тогда задача отсчёта, запущенная для старого раунда, не может случайно начать новый. Проверка «игра активна» слишком общая: активной уже может быть другая сессия.
Пример двух почти одновременных событий
Игрок закончил задачу и сразу отключился. Обработчик успеха переводит RUNNING в FINISHING, а обработчик выхода пытается завершить ту же сессию. Вторая операция должна увидеть принятое решение и выполнить только предусмотренную очистку. Она не меняет победу на отказ и не выдаёт вторую награду. Правило приоритетов причин определите до реализации.
Несколько событий одного действия не должны создавать несколько решений раунда. Перевод событий в результат.
Сделайте вход в игру отдельной операцией
До выдачи снаряжения проверьте авторизацию, отсутствие другой сессии, доступный слот и готовность мира. Сохраните исходное состояние предусмотренным владельцем инвентаря. Только после подтверждения переходите к перемещению и игровому набору. Если строительство арены не завершилось, игроку нужно показать ожидание, а не телепортировать в неполную комнату.
Для каждой частичной стадии выберите компенсацию. Не удалось сохранить данные — вход отменяется без очистки вещей. Не удалось телепортировать после сохранения — исходное состояние остаётся доступным для восстановления. Не удалось создать арену — слот освобождается и новая сессия не публикуется. Такой разбор важнее длинного метода, который считает любую неудачу одним исключением.
Снимок исходного игрока должен переживать сбой между очисткой и завершением игры. Сохранение и возврат вещей.
Завершение разбейте на подтверждённые этапы
Сначала закройте приём новых игровых действий и зафиксируйте причину. Затем запишите результат и намерение награды. После этого возвращайте игрока и очищайте принадлежащие сессии сущности, блоки и задачи. Если награда внешняя, её журнал может завершаться отдельно: очистка мира не должна стирать незавершённое начисление.
У таймеров сохраняйте владельца и отменяйте их при выходе из состояния. Однако отмена сама по себе не заменяет проверку поколения внутри callback: уже запланированный вызов может успеть начать выполнение. Две защиты описывают разные границы. Та же проверка нужна для асинхронного ответа базы или построителя комнаты.
- Переход в FINISHING принимается только один раз.
- Поздние удары и таймеры больше не меняют счёт.
- Результат получает устойчивый идентификатор.
- Возвращается правильное исходное состояние игрока.
- Удаляются только ресурсы завершённой сессии.
- Слот становится доступным после подтверждённой очистки.
Пауза отличается от окончательного выхода
Если режим допускает продолжение, сохраняйте игровой снимок отдельно от исходного. Пауза хранит комнату, цель, время и улучшения, а возвращение на спавн восстанавливает обычное состояние. Окончательный выход закрывает право продолжения. Смерть с платным возрождением — ещё одно явное состояние и не должна обходиться бесплатной паузой.
Для отключения определите срок и место повторного входа. Не оставляйте игрока в мире арены без активной сессии. Либо восстановите документированное состояние, либо завершите его штатно и верните на спавн. «Всё лежит в HashMap» не является решением для рестарта и аварии процесса.
Проверяйте последовательности, а не только победу
Выполните выход на каждом этапе, повторную команду, отключение последнего игрока, рестарт после записи результата и сбой очистки. Для логики переходов полезны небольшие тесты без Minecraft. Для инвентаря, сущностей и телепорта нужен серверный сценарий. В журнале указывайте runId, старое и новое состояние, причину и итог операции.
Если общая арена используется несколькими сессиями, проверяйте владение ресурсами до удаления. Не очищайте всех мобов мира или весь диапазон координат по завершению одного участника. У бесконечной башни ограниченные физические слоты могут переиспользоваться, но только после безопасного перемещения и освобождения прежней комнаты.



