Перейти к содержимому


- - - - -

Не думай о событиях свысока!

bukkit events plugins

- Какое главное событие в моей жизни?!
- шепотом Впереди...
- Главное событие в вашей жизни еще впереди!
к/ф "Собачье сердце"

Разрабатывая бесконечную, как мир Майнкрафта, тему написания плагинов, нельзя объехать на хромой кобыле обойти вниманием столь важный момент, как обработка событий Баккита. Собственно, события и их обработка - это основная причина, по которой в качестве Майнкрафт-сервера выбирают именно платформу Баккит (и ее производные). Без них разработка плагинов становится делом не то что муторным, а по сути невозможным. Думаю, процентов 90, а то и 95 действий плагинов сводятся к обработке того или иного баккитовского события. А все остальное - тоже либо подготовка к этому, либо обеспечение этих действий. В дальнейшем для упрощения повествования события баккита будут именоваться просто "событиями", без уточнения источника (хотя события могут генерить и другие компоненты сервера).

События сами по себе - это не скрижали, посланные свыше, а просто объекты (как и все в языке Ява), производные от класса org.bukkit.event.Event. В какой-то момент, при какой-то ситуации, создается объект события определенного типа и запускается в обработку. Появление данного события в обработке как раз и сигнализирует о возникновении некой ситуации на сервере. То есть события - это индикаторы возникновения некой ситуации (штатной или не очень), соответствующей данному типу событий.
Тех, кто привык к системе обработке событий Windows, где события кидаются в общую очередь, и далее все, кто заинтересован в их обработке, должны периодически обращаться к менеджеру этой очереди с вопросом "Есть чо?", поначалу, наверное, будут немного напрягаться от методики, принятой в бакките: здесь менеджер событий при поступлении нового объекта бросает все дела, тормозит всех, вытаскивает список подписчиков данного события и начинает тормошить их всех вопросом "Ннада?". Сложно сказать, какой метод лучше или хуже, и у того и у другого есть свои плюсы и минусы. Тем более, что процесс обработки я описал только черновыми штрихами, без конкретики. Например, событие можно объявить асинхронным, что, по идее, вынесет его обработку в отдельный поток.

Самое главное, что должен уметь делать класс, ответственный за обработку события (если, конечно, он не абстрактный), это выдавать по запросу список обработчиков (подписчиков, слушателей, наблюдателей за наблюдающими) данного события. Как понятно из контекста, список этот должен быть статическим, то есть привязанным именно к классу, а не к конкретному объекту события:
public class BlaBlaEvent extends Event
{
    private static final HandlerList handlers = new HandlerList();
 
    public HandlerList getHandlers()
    {
        return BlaBlaEvent.handlers;
    }
}
Обратите внимание: несмотря на то, что сам список обработчиков является статическим, метод getHandlers(), возвращающий его, не является статическим. В этом просто нет особой необходимости, поскольку он используется при обработке конкретного экземпляра события данного бла-бла-типа (при регистрации обработчиков баккит действует через функции пакета java.lang.reflect, которым статичность свойств и методов до одного места). В общем и целом, как раз именно этот список и является тем самым списком обработчиков, по которому проходится менеджер событий баккита с вопросом "Ннада?".

Еще одной немаловажной особенностью многих баккитовских событий является возможность их отмены. Конечно, далеко не все события обладают такой способностью всилу естественных причин: например, какой смысл делать отменяемым событие PlayerQuitEvent, если игрок все равно уже вышел с сервера, хотите вы этого или нет. Однако, например, PlayerMoveEvent вполне логично сделано отменяемым, чтобы можно было в зависимости от текущих условий отказать игроку в перемещении в заданную точку. Некоторые события, как например PlayerLoginEvent, реализуют механизм отмены собственными методами (allow и disallow) - флаг Гондураса им для этого в руки. Однако если событие следует классической схеме отмены, оно должно наледовать интерфейс Cancellable и реализовывать его методы isCancelled и setCancelled:
public class BlaBlaEvent extends Event
{
    private static final HandlerList handlers= new HandlerList();
    private boolean cancel= false;
 
    public HandlerList getHandlers()
    {
	    return BlaBlaEvent.handlers;
    }
 
    public boolean isCancelled()
    {
        return this.cancel;
    }
 
    public void setCancelled(boolean cancel)
    {
        this.cancel= cancel;
    }
}
Смысл механизма отмены разъясню чуть ниже.

В остальном состав методов и полей класса описания события отдан на откуп разработчику, вы можете добавлять нужные вам свойства, аргументы конструктора события (поскольку создавать его экземпляры будете сами).

Всего в окружении баккита генерится несколько десятков видов событий, разбитых на глобальные группы (производные от глобальных классов): события игрока (PlayerEvent), события блоков (BlockEvent), события сущностей (EntityEvent), события мира (WorldEvent), общие события сервера (ServerEvent), события погоды (WeatherEvent) и еще несколько несистематизированных событий. В целом они покрывают 99% потребностей разработчиков плагинов, хотя иногда бывает чертовски обидно, когда не находишь даже примерно подходящего события, за которое можно было бы зацепиться: например, как уже говорилось в предыдущих статьях, полная засада ожидает при попытке отловить проведение сделки с деревенским жителем.

В любом случае, как бы ни был объявлен класс события, какие бы аргументы он ни содержал в конструкторе, рано или поздно все заканчивается созданием экземпляра этого события и запуском его в обработку:
BlaBlaEvent event= new BlaBlaEvent(...);
    Bukkit.getServer().getPluginManager().callEvent(event);
Это как раз та самая точка, где генерится сообщение-индикатор возникшей ситуации. Располагаться она может в любом месте кода вашего плагина. В первой строчке приведенного примера создается экземпляр нужного класса события. Второй строчкой мы отправляем созданный объект события менеджеру событий баккита (который по совместительству является и менеджером плагинов баккита).

Поскольку все пространство классов java-машины едино, вы (или кто-то еще) можете получить доступ к обработке абсолютно любого события (если, конечно, оно объявлено как public). Таким образом, создание кастомных событий интересно больше не вам самому внутри собственного плагина (у вас и так море возможностей для внутриплагинного взаимодействия компонентов), а скорее для того, чтобы предоставить возможность разработчикам сторонних плагинов получить доступ к "достижениям" вашего плагина.

А вот теперь настало время взглянуть на проблему с другой стороны, поскольку нас, как правило, будет интересовать не генерация событий, а их обработка. Давайте присмотримся к этому механизьму поближе. Как уже было сказано выше, менеджер событий опрашивает все слушателей... как же нам попасть в ряды этих самых слушателей? Для этого вы должны выполнить 3 следующих действия:

1. Класс, методы которого будут заниматься обработкой тех или иных событий, должен реализовывать интерфейс Listener. неважно, будет ли это ваш главный ласс плагина или один из второстепенных, но "заточенных" именно под это. Пускай это (для простоты) будет основной класс нашего тестового плагина:
public class TestPlugin extends JavaPlugin implements Listener
{
    ...
}
2. Каждый, метод, промышляющий обработкой событий, должен иметь один единственный параметр, параметр этот должен быть производным от класса Event. Собственно, класс этого параметра как раз и определяет - какой тип событий будет обрабатывать данный метод. Кроме того, метод-обработчик должен предваряться директивой @EventHandler (о директивах как-нибудь постараюсь написать чуть подробнее):
@EventHandler
public void onBlaBla(BlaBlaEvent event)
{
    ...
}
Название метода значения не имеет (можно даже в 1 классе создать несколько методов, обрабатывающих одно и то же событие). Однако хорошим тоном считается начинать название метода с "on" и заканчивать названием класса события без "Event".

3. "Где-нибудь" (обычно при запуске, в методе onEnable класса плагина) необходимо произвести регистрацию объекта класса-обработчика событий (обратите внимание: не метода, а именно объекта класса) в менеджере событий баккита:
@Override
public void onEnable()
{
    getServer().getPluginManager().registerEvents(this, this);
}
Первым аргументом вызова метода registerEvents является класс-обработчик событий. Вторым - плагин, содержащий данный класс. Поскольку в нашем случае и то и другое являются одним и тем же объектом, а вызов производится из внутреннего метода, в качестве обоих аргументов выступает this.

В очень упрощенном виде алгоритм работы регистратора событий баккита (на самом деле - регистратора обработчиков событий) выглядит примерно так: указанный класс просматривается на предмет поиска методов, помеченных директивой @EventHandler и имеющих в качестве единственного аргумента объект класса, производного от Event. При нахождении такого метода менеджер получает список обработчиков handlers такого класса события и добавляет к этому списку найденный метод (на самом деле, там особо извращенным способом создается специальный метод-исполнитель, но это уже никому не интересные детали). Таким образом, каждый метод, помеченный директивой @EventHandler класса, производного от Listener, ставится в очередь (- В очередь, сукины дети, в очередь!! (с)) подписчиков того или иного типа (класса) событий.

И вот тут самое время вернуться к свойству отмены события, а также рассмотреть систему приоритетов. В самом общем виде директива @EventHandler объявляется следующим образом:
@EventHandler(ignoreCancelled= true|false, priority= EventPriority.XXXX)
Параметр ignoreCancelled (по-умолчанию false) сообщает, должен ли обработчик получать т.н. "отмененные" события. Как было описано выше, для сообщений, реализующих интерфейс Cancellable, можно установить (через вызов setCancelled) признак отмены данного события. После этого данное событие будет передаваться только обработчикам, для которых установлен признак ignoreCancelled (true).

Свойство priority может принимать следующие значения: LOWEST, LOW, NORMAL (по-умолчанию), HIGH, HIGHEST, MONITOR. Как понятно из контекста, наименьший приоритет имеют обработчики LOWEST, наибольший - нет, не HIGHEST, - MONITOR. Их в документации настоятельно рекомендуют использовать только для регистрации самого факта наступления события, без попытки его изменения. Например, это может пригодиться для логгера действий игрока (см. Prism, LogBlock, CoreProtect). В целом, если нет веских причин, следует использовать значение NORMAL.

В свете вышесказанного, алгоритм действий менеджера при обработке того или иного события может быть примерно следующим: список обработчиков события просматривается несколько раз, сначала событие передается обработчикам, имеющим статус приоритета MONITOR, затем обработчикам с приоритетам HIGHEST, и так далее до LOWEST. При этом, если событие наследует интерфейс Cancellable и в какой-то момент оно помечено как отмененное, в дальнейшем оно передается только обработчикам, у которых признак ignoreCancelled равен true. Такие вот пирожки с котятами.

Что касается очередности плагинов в пределах одного уровня приоритета, то тут, подозреваю (специально не смотрел но осуждаю), первым в очереди оказывается тот, кто первым встал зарегистрировался как обработчик данного события.

Что же можно сделать при обработке события? Да что угодно. Можно, например, при обработке события перемещения игрока проверять его попадание в определенный регион и по достижении его выдавать соответствующее сообщение. Следует помнить лишь о том, что обработка большинства событий производится в синхронном режиме (в общем потоке сервера, с приостановкой всех остальных действий), что потенциально может привести к лагам, а в исключительных случаях - к падению сервера (как правило, через 15-30 секунд зависания). Так что при выполнении операций, требующих значительных временных ресурсов, выделяйте подобные действия в отдельный поток (постараюсь потом поподробнее остановиться на классе Runnable и всем, что с ним связано).

Что я помимо этого имею сказать за события: кроме описанных выше классических событий, я бы выделил еще т.н. псевдособытия: для некоторых баккитовких классов (в основном это касается класса JavaPlugin) предусмотрен ряд шаблонных методов, название которых, как и рекомендуемое мной для обработчиков событий, начинается с "on". Эти методы также вызываются в конкретных ситуациях и служат индикаторами их наступления, а, соответственно, при наследовании могут быть напрямую использованы как обработчики данных "событий". В частности, для JavaPlugin определены следующие подобные методы:


- onCommand - запускается при получении плагином консольной команды (команда должна быть описана в файле plugin.yml в секции commands (см. предыдущую статью). Особо обращаю внимание: если за плагином не зарезервирована конкретная команда, вызова данного метода не произойдет.
- onTabComplete - вызывается в ситуации, когда пользователь нажимает клавишу Tab после ввода команды. Можно использовать для того, чтобы отфильтровать список команд, которые стоит показывать пользователю в качестве подсказки, а которые нет.
- onLoad - вызывается при загрузке плагина (не путать с onEnable, о различиях между этими моментами рассказано в предыдущей статье).
- onEnable - вызывается в момент запуска плагина (вызовом setEnabled(true|false) можно управлять данным процессом в зависимости от проведенных проверок. Однако будьте внимательны, в зависимости от значения аргумента могут повторно вызываться методы onEnable или onDisable, что потенциально может привести к зацикливанию и крашу сервера).

- onDisable - вызывается в момент остановки плагина (по команде /reload, ныне репрессированной, или в момент остановки сервера).


Данные псевдособытия, хоть и не попадают под каноническое описание, тем не менее позволяют без особых затрат для простейших плагинов реализовать базовые действия. А уж методы onEnable и onDisable по-любому перекрываются всеми плагинами.

Вот, наверное, и все по самым основным сведениям о событиях баккита. К сожалению, в масштабах одной статьи (уже немаленькой) невозможно рассказать о всех ньюансах, проблемах и затыках обработки событий. Но, по крайней мере, базовые навыки по этому вопросу вы получили. В следующий раз обратимся к консольным командам сервера.
  • Hedgehog1024, JesterFild, avttrue и еще 2 это одобряют


3 Комментарии

Великолепные статьи. Спасибо!

Великолепные статьи. Спасибо!

стараемся потихоньку

Поправочка из серии "Век живи - век учись. Дураком помрешь": в конце статьи предложил непроверенный способ регистрации обработчика событий непосредственно для Event. Как выяснилось на практике, такая схема не работает всилу абстрактности данного класса:

 

 

org.bukkit.plugin.IllegalPluginAccessException: Unable to find handler list for event org.bukkit.event.Event. Static getHandlerList method required!

 

В общем, мимо.