Войти или зарегистрироваться

Стартап BukkitScheduler - таймеры, кулдауны, потоки

imDaniX

Активный участник
Пользователь
Баллы
96
Имя в Minecraft
imDaniX
Что едим?​
Или, проще говоря, что такое BukkitScheduler и как им пользоваться.
BukkitScheduler (далее - просто шедулер) по сути, выполняет две роли: во-первых, позволяет разграничивать ваши задачи по времени; во-вторых, позволяет назначать задачи как на основной поток (в котором выполняется основная логика игры), так и на асинхронный.

Ещё, в предисловии хочу упомянуть - для работы с шедулером вам понадобится экземпляр вашего плагина. Для его получения многие из вас наверняка знакомы со статичным синглтоном (static singleton) - этот пресловутый getInstance(). Просто скажу, что так делать не стоит - это ломает структуру проекта, и зачастую очень негативно влияет на неё при расширении. Вместо этого, посмотрите в сторону простого внедрение зависимости (dependency injection). Пример использования есть в послесловии.

С чем едим?​
В сущности, шедулер имеет три метода принимающих Runnable, которые вы так или иначе будете использовать, плюс их асинхронные варианты, и на каждый прибавляется вариант с Consumer<BukkitTask>.
Для чуть лучшего понимания, если не знакомы с Runnable - это интерфейс, представляющий из себя лишь один метод run(). Когда мы передаем его в шедулер, тот позже просто выполняет этот метод. Посмотрим на небольшом примере:
PHP:
Server server = plugin.getServer();
Runnable action = () -> {
    server.broadcastMessage("Это сообщение будет отправлено вторым!");
    server.broadcastMessage("Это сообщение будет отправлено третьим!");
};
server.broadcastMessage("Это сообщение будет отправлено первым!");
action.run();
Соответственно, в чате мы сначала увидим "Это сообщение будет отправлено первым!", и лишь потом "...вторым!" и "...третьим!". То есть, Runnable это некий код, который мы можем вызвать в любой момент отдельно. Таким образом и работает BukkitScheduler - он вызывает наш код, когда приходит его время.

В этом туториале в качестве Runnable и Consumer<BukkitTask> как правило будут использоваться лямбда-выражения - если очень кратко, это микро-классы, которые имеют лишь один метод, и при написании видно лишь его аргументы в круглых скобках. Пример уже был выше - так как у Runnable#run() нет аргументов вовсе, скобки вообще пустые. У Consumer<BukkitTask> там будет один аргумент с типом BukkitTask. Если есть проблемы с английским языком, или материал попросту непонятен, можете погуглить на русском - статей, благо, полно.

Пометка: в одной секунде обычно 20 тиков, соответственно, 1 тик ~= 50 миллисекунд. При лагах сервера это значение может увеличиваться. Мы будем изначально ставить время в секундах, и просто умножать их на 20L (L значит тип long вместо int по умолчанию). Для лучшего понимания время перемещено в отдельные переменные, но никто не мешает использовать числа на месте.

runTask(Plugin plugin, Runnable task) - запустит задачу Runnable на следующем тике сервера.
PHP:
Server server = plugin.getServer();
BukkitTask task = server.getScheduler().runTask(plugin, () -> {
    server.broadcastMessage("Это сообщение покажется примерно через 50мс!");
    // TODO: Остальной код
});


runTaskLater(Plugin plugin, Runnable task, long delay) - запустит задачу Runnable через указанное в параметре delay количество тиков.
PHP:
Server server = plugin.getServer();
int delay = 5;
BukkitTask task = server.getScheduler().runTaskLater(plugin, () -> {
    server.broadcastMessage("Это сообщение покажется примерно через 5 секунд!");
    // TODO: Остальной код
}, delay * 20L);


runTaskTimer(Plugin plugin, Runnable task, long delay, long period) - запустит задачу Runnable через указанное в параметре delay количество тиков, повторяя её раз в period тиков.
PHP:
Server server = plugin.getServer();
int delay = 5;
int period = 42;
BukkitTask task = server.getScheduler().runTaskTimer(plugin, () -> {
    server.broadcastMessage("Это сообщение покажется примерно через 5 секунд и будет повторяться каждые 42 секунды!");
    // TODO: Остальной код
}, delay * 20L, period * 20L);


Пометка: совсем не обязательно использовать Server server = plugin.getServer(); - эту строку можно стереть, и вместо переменной server просто использовать класс Bukkit. Здесь оно используется в рамках "лучшей практики", но так как BukkitAPI сам предлагаем вам все методы Server, дело за вами. Сохранять переменную BukkitTask тоже не требуется. То есть, при самом минимуме, последний пример может выглядеть и так:
PHP:
Bukkit.getScheduler().runTaskLater(plugin, () -> {
    Bukkit.broadcastMessage("Это сообщение покажется примерно через 5 секунд и будет повторяться каждые 42 секунды!");
    // TODO: Остальной код
}, 100L, 840L);


Все используемые методы будут запускать задачу на основном потоке. У каждого метода есть свой асинхронный вариант - соответственно, он будет запускать нашу задачу в отдельном потоке. Называются они точно также, только с припиской ...Asynchronously. Это полезно при выполнении тяжелых или долгих операций, вроде просчета большого количества данных, работы с файлами или обращения к базе данных. Важно подметить, что в асинхронном потоке нельзя взаимодействовать с миром - блоки, мобы, инвентари, и даже команды - для этого вам нужно провести операцию в основном потоке, для чего и пригодится самый первый runTask.

Все упомянутые методы возвращают BukkitTask. На практике, его основная роль - остановка выполнения задачи. Например, если вы хотите в какой-то момент остановить выполнение runTaskTimer, достаточно сохранить его в переменную и просто использовать task.cancel(). Если вам не требуется это, можете, соответственно, просто не сохранять значение в переменную.
Наконец, если вы хотите остановить задачу в какой-то момент, но не хотите сохранять BukkitTask, воспользуйтесь вариантом методов с Consumer<BukkitTask> - он предоставляет экземпляр BukkitTask непосредственно в вашу задачу, из которой его можно остановить. Методы с ним называются точно также, меняется только второй аргумент.
PHP:
server.getScheduler().runTaskTimer(plugin, (task) -> {
    server.broadcast("Сообщение будет какое-то время повторяться примерно раз в секунду.")
    if (isTimeToStop()) {
        task.cancel();
    }
}, 0L, 20L)
Подразумевается, что метод isTimeToStop() возвращает true, когда задача должна быть остановлена.

Как едим?​
Посмотрим на сценарии, приближенные к реальности.
Например, задача такова: телепортировать игрока, если он просит об этом в чате. AsyncPlayerChatEvent как правило проходит в асинхронном потоке - значит взаимодействовать с миром просто так нельзя, а то получим ошибку.
PHP:
private final Plugin plugin; // Наш плагин
private final Location spawn; // Локация спавна

@EventHandler
public void onChat(AsyncPlayerChatEvent event) {
    if (event.getMessage().contains("Хочу на спавн")) {
        plugin.getServer().getScheduler().runTask(plugin, () -> {
            if (player.isOnline()) player.teleport(spawn);
        });
    }
}
Допустим, задача: игрок может ставить один блок раз в минуту. При этом хотим показывать отсчет до следующей установки. В рамках примера используется Paper вместо Spigot/Bukkit, просто потому что там есть более удобный метод Player#sendActionBar
PHP:
private static final int COOLDOWN_VALUE = 60; // Наш кулдаун в секундах
private final Plugin plugin; // Наш плагин
private final Map<UUID, Integer> timers = new HashMap<>();

@EventHandler
public void onPlace(BlockPlaceEvent event) { // При установке блока
    Player player = event.getPlayer();
    UUID id = player.getUniqueId();
    if (timers.containsKey(id)) { // Проверяем наличие кулдауна
        event.setCancelled(true);
        player.sendMessage(ChatColor.RED + "Вы не можете ставить блоки ещё " + timers.get(id) + " секунд.")
    } else {
        timers.put(id, COOLDOWN_VALUE); // Иначе добавляем
    }
}

public void startTimer() {
    Server server = plugin.getServer();
    server.getScheduler().runTaskTimer(plugin, () -> {
        Iterator<Map.Entry<UUID, Integer>> iterator = timers.entrySet().iterator(); // Проходимся по всем значениям в Map'е
        while (iterator.hasNext()) {
            Map.Entry<UUID, Integer> entry = iterator.next();
            Player player = server.getPlayer(entry.getKey());
            if (player != null) { // Считаем, только если игрок онлайн
                int cooldown = entry.getValue() - 1;
                if (cooldown > 0) {
                    entry.setValue(cooldown);
                    player.sendActionBar("До следующей установки блока " + cooldown + " секунд");
                } else {
                    iterator.remove();
                    player.sendActionBar("Вы можете поставить новый блок!");
                }
            }
        }
    }, 20L, 20L);
}
Подразумевается, что метод startTimer() будет вызван при или сразу после создания экземпляра класса.
Учите - если вам не нужно постоянно знать кулдаун игрока, или делать что-то активно по его истечению, то скорее всего вам не понадобится шедулер. Загляните в послесловие.
Задача: получить сохраненные в БД сообщения игроку по команде. Для простоты получения игрока в данном примере используется pattern matching, доступный с Java 14.
PHP:
private final Plugin plugin; // Наш плагин
private final Database db; // Некая база данных

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
    if (!(sender instanceof Player player)) {
        sender.sendMessage("Данная команда доступна только игрокам!");
        return true;
    }
    BukkitScheduler scheduler = plugin.getServer().getScheduler();
    scheduler.runTaskAsynchronously(plugin, () -> { // Запустим задачу в асинхронном потоке
        player.sendMessage("Загружаем вашу почту...");
        List<String> messages = db.loadMessages(player);
        scheduler.runTask(plugin, () -> { // Вернемся в основной поток
            if (!player.isOnline()) return; // Если игрок офлайн, отправлять данные некому
            player.sendMessage("У вас " + messages.size() + " писем.")
            messages.forEach(player::sendMessage);
        });
    });
    return true;
}
Подразумевается, что Database#loadMessages возвращает список сообщений, отправленных игроку.
Данные отрывки кода - лишь примеры применения шедулера, и не должны быть использованы как есть. Места для оптимизации и улучшения здесь полно, но для показа работы с шедулером - пойдет. Буду рад добавить и ваши примеры, если они будут достаточно показательны.

Весь код писался от руки и может содержать ошибки. Если кто-чего заметит - буду рад исправить.
 
Последнее редактирование:
Внедрение зависимостей​
Вместо
PHP:
public final class CoolPlugin extends JavaPlugin { // Наш плагин
    private static final CoolPlugin instance;

    public static CoolPlugin getInstance() {
        return instance;
    }

    @Override
    public void onEnable() {
        instance = this;
        MyCoolClass coolClass = new MyCoolClass();
        coolClass.methodWithPlugin();
    }
}

public class MyCoolClass { // Класс, в котором требуется наш плагин
    public void methodWithPlugin() {
        CoolPlugin plugin = CoolPlugin.getInstance();
        // TODO: Остальной код
    }
}
лучше попробовать
PHP:
public final class CoolPlugin extends JavaPlugin { // Наш плагин
    @Override
    public void onEnable() {
        MyCoolClass coolClass = new MyCoolClass(this);
        coolClass.methodWithPlugin();
    }
}

public class MyCoolClass { // Класс, в котором требуется наш плагин
    private final CoolPlugin plugin;

    public MyCoolClass(CoolPlugin plugin) {
        this.plugin = plugin;
    }

    public void methodWithPlugin() {
        // Переменная plugin уже доступна
        // TODO: Остальной код
    }
}
Да, когда нам плагин требуется в разных местах, кода придется писать немного больше. Но как правило, это окупается в виде более структурированного и читабельного кода, который будет куда проще расширять и тестировать в будущем.

Почему Thread.sleep(...) не подходит?​
Суть в том, что при использовании Thread.sleep(...) вы замораживаете не просто участок кода - вы останавливаете работу всего потока. Это может сработать, если вы выполняете этот метод в асинхронном потоке (но делать так всё равно не стоит), но когда вы, например, пытаетесь сделать это при установке блока - вы буквально замораживаете работу всего сервера, так как вся основная логика игра проходит в одном потоке.

Офтоп о кулдаунах​
Если вам не требуется постоянное отображение информации о времени, то вам не нужен шедулер - достаточно будет хранить время, до которого продлится КД.
PHP:
private static final long COOLDOWN_VALUE = 60 * 1000; // Наш кулдаун в миллисекундах
private final Map<UUID, Long> cooldowns = new HashMap<>();

@EventHandler
public void onPlace(BlockPlaceEvent event) {
    Player player = event.getPlayer();
    UUID id = player.getUniqueId();
    long now = System.currentTimeMillis();
    if (cooldowns.containsKey(id)) {
        long until = cooldowns.get(id);
        if (until > now) { // Если время ещё не пришло
            event.setCancelled(true);
            long diff = (until - now) / 1000; // Переводим в секунды
            player.sendMessage("Вы не сможете поставить блок ещё " + diff + " секунд.");
            return;
        }
    }
    cooldowns.put(id, now + COOLDOWN_VALUE);
}
На самом деле, в данном случае вместо Map можно воспользоваться PersistentDataHolder API, но это уже совсем другая тема.
Примерно таким же образом можно реализовать, например, систему временных мутов.

Полезные утилиты​
 
Последнее редактирование:
Здесь когда-нибудь будет статья по шедулерам Folia.
 
Последнее редактирование:
Спасибо большое за информацию с примерами!
 
Ну наконец-то запилил полноценный актуальный туториал по шедулерам!

От себя ещё могу предложить инструмент, позволяющий запускать несколько операций последовательно с задержкой без вложения шедулеров друг в друга. Например:
PHP:
final TasksQueue queue = new TasksQueue()
    .action(() -> {
        player.sendMessage("[#1] Time in default world: " + Bukkit.getWorlds().get(0).getTime());
    })
    .sleepTicks(20)
    .action(() -> {
        player.sendMessage("[#2] Time in default world: " + Bukkit.getWorlds().get(0).getTime());
    })
    .sleep(3, TimeUnit.SECONDS)
    .action(() -> {
        player.sendMessage("[#3] Time in default world: " + Bukkit.getWorlds().get(0).getTime());
    });
queue.start(this.plugin);

Результат выполнения:
Код:
[#1] Time in default world: 17886
[#2] Time in default world: 17906
[#3] Time in default world: 17966

Класс TasksQueue: https://pastebin.com/cyKQ550c
 
Последнее редактирование:
Ну наконец-то запилил полноценный актуальный туториал по шедулерам!
Тоже удивлен, что до сих пор не было. Конечно, базис, но часто не хватает места, куда можно прям пальцем тыкнуть :D
От себя ещё могу предложить инструмент, позволяющий запускать несколько операций последовательно с задержкой без вложения шедулеров друг в друга. Например:
PHP:
final TasksQueue queue = new TasksQueue()
    .action(() -> {
        player.sendMessage("[#1] Time in default world: " + Bukkit.getWorlds().get(0).getTime());
    })
    .sleepTicks(20)
    .action(() -> {
        player.sendMessage("[#2] Time in default world: " + Bukkit.getWorlds().get(0).getTime());
    })
    .sleep(3, TimeUnit.SECONDS)
    .action(() -> {
        player.sendMessage("[#3] Time in default world: " + Bukkit.getWorlds().get(0).getTime());
    });
queue.start(this.plugin);

Результат выполнения:
Код:
[#1] Time in default world: 17886
[#2] Time in default world: 17906
[#3] Time in default world: 17966

Класс TasksQueue: https://pastebin.com/cyKQ550c
А вот это круто. Позже, наверно, к резерву прикреплю.
 
Можете подсказать что не так? В общем использовал Ваш код "Создаём и отображаем кулдаун", пока не переделывал под себя, решил проверить как в оригинале, запустил сервер поставил блок, оно все запустились, но вот только кулдаун не исчезает, а при попытке даже после 60 секунд поставить блок, выводит сообщение До следующей установки блока 60 секунд. Подскажите как исправить пожалуйста.
 
Можете подсказать что не так? В общем использовал Ваш код "Создаём и отображаем кулдаун", пока не переделывал под себя, решил проверить как в оригинале, запустил сервер поставил блок, оно все запустились, но вот только кулдаун не исчезает, а при попытке даже после 60 секунд поставить блок, выводит сообщение До следующей установки блока 60 секунд. Подскажите как исправить пожалуйста.
Впрочем уже исправил, но все равно спасибо)
 
Можете подсказать что не так? В общем использовал Ваш код "Создаём и отображаем кулдаун", пока не переделывал под себя, решил проверить как в оригинале, запустил сервер поставил блок, оно все запустились, но вот только кулдаун не исчезает, а при попытке даже после 60 секунд поставить блок, выводит сообщение До следующей установки блока 60 секунд. Подскажите как исправить пожалуйста.
Впрочем уже исправил, но все равно спасибо)
На всякий случай уточню - проблема с кодом из примера, или на твоей стороне что-то? Если беда с примером - буду рад поправить, ибо писалось без толковых проверок. Но звучит так, будто startTimer() не был вызван.
 
На всякий случай уточню - проблема с кодом из примера, или на твоей стороне что-то? Если беда с примером - буду рад поправить, ибо писалось без толковых проверок. Но звучит так, будто startTimer() не был вызван.
startTimer() да, ну я понял что это я его не вызвал, ещё, строка
Iterator<Map.Entry<UUID, Integer>> iterator = timers.entrySet();
выдавала ошибку, в конце нужно было добавить .iterator(), т.е. чтобы получилось
Iterator<Map.Entry<UUID, Integer>> iterator = timers.entrySet(). iterator();
И все прекрасно работает.
 
На всякий случай уточню - проблема с кодом из примера, или на твоей стороне что-то? Если беда с примером - буду рад поправить, ибо писалось без толковых проверок. Но звучит так, будто startTimer() не был вызван.
Кстати, не подскажете как бы качественно организовать кулдаун таким способом под разные команды?
 
startTimer() да, ну я понял что это я его не вызвал, ещё, строка
Iterator<Map.Entry<UUID, Integer>> iterator = timers.entrySet();
выдавала ошибку, в конце нужно было добавить .iterator(), т.е. чтобы получилось
Iterator<Map.Entry<UUID, Integer>> iterator = timers.entrySet(). iterator();
И все прекрасно работает.
Спасибо, поправил.
Кстати, не подскажете как бы качественно организовать кулдаун таким способом под разные команды?
Рекомендовал бы расширить вариант из последнего спойлера "Офтоп о кулдаунах". Можно набросать какой-нибудь такой класс для обработки кулдаунов:
PHP:
public class Cooldowns {
    private final Map<UUID, Long> cooldowns;

    public Cooldowns() {
        this(false);
    }

    public Cooldowns(boolean concurrent) {
        this.cooldowns = concurrent ? new ConcurrentHashMap<>() : new HashMap<>();
    }

    public long tryCooldown(UUID player, long seconds) {
        long now = System.currentTimeMillis();
        Long cd = cooldowns.get(player);
        if (cd == null || cd >= now) { // Кулдауна ещё нет ИЛИ он прошел
            cooldowns.put(player, now + (seconds * 1000L));
            return 0;
        } else { // Иначе возвращаем текущий кулдаун в миллисекундах
            return cd;
        }
    }
 
    public boolean hasCooldown(UUID player) {
        long now = System.currentTimeMillis();
        Long cd = cooldowns.get(player);
        if (cd != null) {
            if (now > cd) {
                return true;
            }
            removeCooldown(player);
        }
        return false;
    }
 
    public void removeCooldown(UUID player) {
        cooldowns.remove(player);
    }
}
(concurrent для случая, если будешь работать меж потоков)
Далее, если команда твоя, хранить экземпляр Cooldowns в её классе. Если нужно для каких-то других команд, можно попробовать ловить через PlayerCommandPreprocessEvent и хранить Map<String, Cooldowns> - заполнить заранее с нужными командами в качестве ключа. В идеале следует от мусора избавляться - в классе Cooldowns где-нибудь добавить возможность проитерировать значения и удалить ненужные, да запускать это через шедулер раз в час.
 
Последнее редактирование:
Спасибо, поправил.

Рекомендовал бы расширить вариант из последнего спойлера "Офтоп о кулдаунах". Можно набросать какой-нибудь такой класс для обработки кулдаунов:
PHP:
public class Cooldowns {
    private final Map<UUID, Long> cooldowns;

    public Cooldowns() {
        this(false);
    }

    public Cooldowns(boolean concurrent) {
        this.cooldowns = concurrent ? new ConcurrentHashMap<>() : new HashMap<>();
    }

    public boolean tryCooldown(UUID player, long seconds) {
        long now = System.currentTimeMillis();
        Long cd = cooldowns.get(player);
        if (cd == null || cd >= now) { // Кулдауна ещё нет ИЛИ он прошел
            cooldowns.put(player, now + (second * 1000L));
            return true;
        } else {
            return false;
        }
    }
 
    public boolean hasCooldown(UUID player) {
        long now = System.currentTimeMillis();
        Long cd = cooldowns.get(player);
        if (cd != null) {
            if (now > cd) {
                return true;
            }
            removeCooldown(player);
        }
        return false;
    }
 
    public void removeCooldown(UUID player) {
        cooldowns.remove(player);
    }
}
(concurrent для случая, если будешь работать меж потоков)
Далее, если команда твоя, хранить экземпляр Cooldowns в её классе. Если нужно для каких-то других команд, можно попробовать ловить через PlayerCommandPreprocessEvent и хранить Map<String, Cooldowns> - заполнить заранее с нужными командами в качестве ключа. В идеале следует от мусора избавляться - в классе Cooldowns где-нибудь добавить возможность проитерировать значения и удалить ненужные, да запускать это через шедулер раз в час.
Спасибо большое!
 
Апну. Добавил немного информации про Runnable, перенес некоторые вставки во второй пост, дополнил его информацией про Thread.sleep(...) и утилитах.
 
Наткнулся на статью по теме на SpigotMC. Забавно, но структурно выполнено практически точно также.
 
Не приведет к отставанию, большое количество таймеров? Допустим у меня есть таймер, где у меня каждый тик выполняется обновление здоровья по кастомной логике и такой же, но с изменением голода. Удобнее было бы разделить их на 2 класса, но стоит ли?
 
Не приведет к отставанию, большое количество таймеров? Допустим у меня есть таймер, где у меня каждый тик выполняется обновление здоровья по кастомной логике и такой же, но с изменением голода. Удобнее было бы разделить их на 2 класса, но стоит ли?
К отставанию может приводить в целом нагрузка на основной поток. Таким образом, продолжительность тика может быть больше 50-и мс.
Если тебя волнует вопрос, не будет ли очередь задач шедулера формировать задержку в тиках - то нет, не будет. Ты можешь запустить хоть тысячу шедулеров, и все они буду отрабатывать в одни и те же тики сервера.

Разделить логику на 2 класса вполне можно, люди сотнями шедулеры запускают. Но единственная рекомендация - не создавать по шедулеру на каждого игрока, а использовать одну задачу на всех онлайн-игроков или по задаче на каждый игровой мир. Иначе есть риск раздуть кол-во шедулеров до нескольких тысяч при онлайне или просто-напросто получить утечку шедулеров, если не останавливать их при выходе игроков
 
Реализовал таймеры, которым можно уже после запуска менять период выполнения. Пример использования:
PHP:
sender.sendMessage("Задача запущена");
new ChangeablePeriodTask() {
    long previousCycleTicks = getCurrentTicks();

    @Override
    public void runCycle() {
        long currentTicks = getCurrentTicks();
        sender.sendMessage("Задача отработала (прошло тиков: " + (currentTicks - this.previousCycleTicks) + ")");
        this.previousCycleTicks = currentTicks;

        if (this.getPeriodTicks() <= 1) {
            this.cancel();
        } else {
            this.setPeriodTicks(this.getPeriodTicks() / 2);
        }
    }
}.runTaskTimer(this.plugin, 200, 200); // Начальный период 10 секунд

Результат выполнения:
Код:
[16:48:07 INFO]: Задача запущена
[16:48:17 INFO]: Задача отработала (прошло тиков: 200)
[16:48:23 INFO]: Задача отработала (прошло тиков: 100)
[16:48:25 INFO]: Задача отработала (прошло тиков: 50)
[16:48:27 INFO]: Задача отработала (прошло тиков: 25)
[16:48:27 INFO]: Задача отработала (прошло тиков: 12)
[16:48:28 INFO]: Задача отработала (прошло тиков: 6)
[16:48:28 INFO]: Задача отработала (прошло тиков: 3)
[16:48:28 INFO]: Задача отработала (прошло тиков: 1)

Класс ChangeablePeriodTask: https://pastebin.com/suNNhAg6

null-safety аннотации удалены
 
Есть ещё вот такая штука для игрищ с CompletableFuture, но её пока не тестировал. Но по логике должно работать корректно:
PHP:
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;

// Author: Dymeth.Me
public class CompletableFutureBukkit<T> extends CompletableFuture<T> {
    private final Executor defaultExecutor;

    public CompletableFutureBukkit(Plugin plugin, boolean async) {
        BukkitScheduler scheduler = plugin.getServer().getScheduler();
        this.defaultExecutor = async
                ? actions -> scheduler.runTaskAsynchronously(plugin, actions)
                : actions -> scheduler.runTask(plugin, actions);
    }

    @Override
    public Executor defaultExecutor() {
        return this.defaultExecutor;
    }
}

null-safety аннотации удалены

Преимущество перед обычным CompletableFuture в том, что такие таски должны останавливаться ядром при выгрузке плагина
 
Последнее редактирование:
Назад
Верх