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


Фотография

Статья: Команда молодости нашей... (с)

bukkit plugins команды

  • Авторизуйтесь для ответа в теме
В этой теме нет ответов

#1 vasco

vasco

    торговец черным деревом на пенсии

  • Администраторы
  • Сообщений: 1784

Написал 11.01.2016 - 00:03

- Всем - стоять! Трамвай - прижаться вправо!
- Пендальф!!!
- Я майор милиции, Заслуженный пожарник Поволжья и Почетный дружинник!
Выходить по одному, сабли на пол, руки за голову!..
Команда была "Стоять!"... Я... сказал... "Стоять!"!!!
х/ф "Две сорванные башни"

Вот есть у нас несколько десятков событий баккита. Казалось бы, что еще нужно, чтобы спокойно встретить старость наладить взаимодействие с игроками? К сожалению, Minecraft - это не Black & White, одними жестами игре не объяснишь, что тебе от нее нужно. Рано или поздно приходится иметь дело с консолью, а именно - с консольными командами. Эти самые команды позволяют предельно формализованно отправить сервер в нужном направлении, пояснив с помощью аргументов - что нужно надеть на шею и что делать при встрече с поездом.

Консольные команды условно можно разделить на "административные", вводимые с консоли сервера (если к ней есть доступ), и "пользовательские", которые обычные игроки применяют, используя строку чата. Деление это достаточно условное, поскольку большинство "административных" команд можно запустить непосредственно из клиента игры через строку чата, так и многие "пользовательские" команды могут быть введены с консоли сервера. Так что деление это обуславливается скорее системой "разрешений" или "пермишенсов" (permissions), установленной на сервере для разных групп игроков (обязательно посвятим этому отдельную статью).

Единственное различие при вводе команд с консоли сервера и через строку общего чата - это использование слеша ("/") в начале строки во втором случае. Что, в общем-то, логично - надо же как-то серверу различать обычный треп в чате между игроками и осмысленные команды, адресуемые именно ему. В дальнейшем для примеров использования команд будет преимущественно использоваться вариант /command, как наиболее наглядный. Однако следует иметь в виду, что при использовании той же команды с консоли сервера ведущий слеш следует опускать. Кстати, ограничений на дальнейшее использование знаков препинания в названии команд не замечено (кроме пробела, который используется для деления строки команды на элементы), чем иногда пользуются разработчики плагинов (см. команды //size, //paste плагина WorldEdit).

Сама команда знаками пробела делится на элементы: первым элементом является имя команды. Далее следуют аргументы команды. Будут это какие-то опции или значения параметров - дело и проблемы исключительно ваши. И возиться с их парсингом и обработкой опять же вам. Как вы будете это делать - каскадами операторов if() либо парсингом строки команды через регулярные выражения - решать снова вам.

Теперь рассмотрим подробнее те ровно два с половиной способа, с помощью которых можно в нашем плагине обработать эти команды. Первый способ, наиболее простой и универсальный, идеально подходит для небольших плагинов с более-менее статичным контентом. Если плагин ваш заточен под решение одной задачи и подразумевает работу всего-лишь с одной-двумя-тремя командами - это как раз ваш случай. Все, что вам необходимо - это наследовать в основном классе вашего плагина метод onCommand (возвращающий false в классе JavaPlugin, от которого и наследуется класс вашего плагина):

/******************************************************************************** * обработка события выполнения команды********************************************************************************/@Overridepublic boolean onCommand(CommandSender sender, Command command, String label, String[] args){	return super[paste][/paste]onCommand(sender, command, label, args);}

Метод onCommand является типичным обработчиком т.н. псевдособытия, которые были описаны в предыдущей статье, посвященной этим самым событиям. Баккит вызывает его в том случае, если в процессе обработки входящих данных встречает команду, подходящую для данного плагина. Что это значит? Это, прежде всего значит, что с помощью данного способа можно получить доступ только к тем командам, на которые есть "подписка" у вашего плагина. Получить эту самую подписку в простейшем случае через файл plugin.yml (также описанный в одной из предыдущих статей), который содержит основную информацию о вашем плагине. Чтобы закрепить за ним какую-либо команду, необходимо в этом файле создать секцию commands (очень подробно описана на bukkit-wiki), а в ней - подсекцию с названием, совпадающим с именем команды:

commands:  test:    description: просто тестовая команда    aliases: [tst, teeeest]    permission: test_plugin[paste][/paste]test    permission-message: Все в сад!    usage: Что я делаю не так?!

Как видно из приведенного куска файла plugin.yml, плагин резервирует за собой команду /test. Тут надо понимать, что никто, в принципе, не накладывает ограничений на пересечение имен команд, описываемых в данной секции. И, в принципе, они могут совпадать с названиями команд, используемыми другими плагинами. Однако при вызове метода onCommand действует вполне логичное правило "Кто первый встал - того и тапки", так что ваш плагин вполне может оказаться с носом, но без тапок, если какой-то другой плагин, "подписанный" на данную команду, в своем методе onCommand при обработке этой же команды уже вернул true. Так что для перехвата "чужих" команд данный метод вряд ли подойдет.

Что касается остальных параметров подсекции описания команды:


    [*]description: содержит произвольную строку описания. Применяется в основном для генерации "хэлпа" командой /help.
    [*]aliases: здесь можно перечислить т.н. псевдонимы вашей команды, которые можно использовать равноценно наряду с основным именем команды. Поскольку перечисление псевдонимов является неименованным списком, его элементы разделяются запятыми и заключаются в прямые скобки (см. статью YML и все-все-все...).
    [*]permission: разрешение, необходимое для использования данной команды. Если разрешение не указано, команда становится доступна всем игрокам сервера. Команды, вводимые с консоли сервера, под действие системы разрешений не попадают - это абсолютный доступ непосредственно от имени сервера.
    [*]permission-message: сообщение, которое будет выдано игроку, не имеющему доступ к данной команду.
    [*]usage: сообщение, которое выдается пользователю, если команда была введена неправильно. Применительно к разработке плагинов это звучит так: если для плагина, который был "подписан" на команду, был вызван метод onCommand для обработки данной команды и он вернул результат false.
    [/list]

    Теперь обратимся к параметрам самого метода onCommand:


      [*]CommandSender sender: объект, отправивший данную команду. Странная система иерархии классов баккита вполне позволяет отправлять команды как от имени игроков или сервера, так и (!) от имени, например, хрюнделя или летящего снежка. Однако обычно отслеживаются только первые два случая - отправка от имени игрока (sender instanceof Player) и отправка команды с консоли (sender instanceof ConsoleCommandSender).
      [*]Command command: сам объект команды. Поскольку методу onCommand достаются только команды, на которые у плагина есть "подписка" (читай, имеющие описание в plugin.yml), баккит по описанию команды генерит объект данного типа, для которого можно получить/установить все те параметры, что я перечислил чуть выше.
      [*]String label: название отправленной команды. Обратите внимание, данный параметр содержит название именно введенной команды, а не команды, описанной в конфиге. То есть, если вы воспользуетесь псевдонимом команды, параметр label будет содержать именно этот псевдоним. Добраться же до основного имени команды можно с помощью конструкции command.getName(). Что интересно, вызов метода command.getLabel() также возвращает основное имя команды, даже если был использован ее псевдоним. Чудны дела твои, Господи.
      [*]String[] args: массив строк, содержащих аргументы команды. Как уже говорилось выше, введенная строка тупо разбивается по пробелам и все, что следует за названием, обзывается аргументами и попадает в этот список. Логично, что массив агрументов может быть и нулевой длины.
      [/list]

      Что же со всем этим делать? Конечно же, обрабатывать. Для начала необходимо определить, о какой именно команде идет речь, поскольку секция commands файла plugin.yml может описывать любое их количество. Если команд предполагается не очень много, можно обойтись последовательными if():

      if (command[paste][/paste]getName()[paste][/paste]equalsIgnoreCase("test1")){    [paste][/paste][paste][/paste][paste][/paste]    return true;}else if (command[paste][/paste]getName()[paste][/paste]equalsIgnoreCase("test2")){​    [paste][/paste][paste][/paste][paste][/paste]    return true;}else [paste][/paste][paste][/paste][paste][/paste]else return false;

      При большем, нежели несколько штук, количестве команд можно перейти к конструкции switch-case. Суть дела от этого сильно не изменится. Следует помнить лишь об одном: в случае удачного парсинга команды ваш метод onCommand должен вернуть true. При этом вы, конечно, можете производить дополнительные проверки на корректность аргументов команды и, в случае ошибки, возвращать отправителю грозные сообщения. Но все же лучше в том случае, если название команды вами распознано, давать понять баккиту, что команда обработана. Также, чтобы вам не приходилось муторно проверять все возможные варианты, лучше всего приводить имя команды к нижнему регистру и использовать именно ее основное имя (getName), а не использовать аргумент label, который может и не совпадать с основным именем.

      Что делать с остальными аргументами команды (массив args), если они, конечно, будут? Тут однозначного ответа нет. Ведь в качестве аргументов команды могут использоваться самые различные по структуре выражения: опции: -option, -option:value, -option:"bla bla value", причем желательно, чтобы опции можно было вставлять в любом месте, а также чтобы можно было использовать сокращенные названия опций, в идеале - чтобы можно было устанавливать перекрестные зависимости между используемыми опциями; целочисленные, дробные аргументы (в том числе и значения опций), строковые выражения (как односложные так и разделенные пробелами), а так же текст до конца строки как единый аргумент. Кроме того, в некоторых случаях команды могут в зависимости от контекста использовать разные наборы аргументов, и эти ситуации также необходимо отслеживать и корректно обрабатывать. Ну и на десерт: каждая команда (а иногда и определенный набор ее параметров) могут требовать отдельного разрешения (пермишенса) на использование.

      В свое время подобная мешанина привела меня к написанию собственного командного процессора, основанного на регулярных выражениях. А поскольку аппетит приходит во время еды, постоянно возникало желание допилить его в сторону универсальности. В итоге все это вылилось в отдельный пакет почти с 40 классами общим объемом под 5к строк. Жаль, что нечто подобное разработчики баккита не заложили в стандартный набор, не пришлось бы изобретать велосипед.

      В целом, если ваша команда предполагает использование простейших аргументов (например, целочисленных x y z), можно, наверное, обойтись простым их парсингом в числа непосредственно из массива аргументов args. А вот если ситуация более запущенная, возможно, лучшим выходом будет объединить все строковые аргументы обратно в строку (через пробел) и далее уже парсить ее своими средствами с учетом контекста.

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

      class CommandHandler implements CommandExecutor{    public boolean onCommand(CommandSender sender, Command command, String label, String[] args)    {        [paste][/paste][paste][/paste][paste][/paste]    }}

      который будет реализовывать метод onCommand интерфейса CommandExecutor, а затем при старте плагина создать объект этого класса и зарегистрировать его как обработчик ваших команд:

      @Overridepublic void onEnable(){    CommandHandler handler= new CommandHandler([paste][/paste][paste][/paste][paste][/paste]);    this[paste][/paste]getCommand("test")[paste][/paste]setExecutor(handler);}

      Обратите внимание, команда, объект которой (PluginCommand) вы пытаетесь получить вызовом метода getCommand, должна быть описана в файле plugin.yml, в противном случае метод вернет null, а следующий за ним вызов setExecutor, соответственно, обрушится эксцепшеном NullPointerException.

      Теперь разберем вторые полтора варианта перехвата вводимых команд, которые представляют собой не что иное, как обычные баккитовские события - PlayerCommandPreprocessEvent и ServerCommandEvent - числом 2 (два штуки). Такая вот шизофрения, случай типичный, описан в "Ярбух фюр психоаналитик унд психопатологик". Как, наверное, понятно из названий классов событий, одно из них отвечает за команды, употребляемые игроками через общий чат (посредством пресловутого "/"), а вторые поступаю в обработку непосредственно с консоли сервера. А происходят они: первое - от PlayerEvent (что позволяет через event.getPlayer() получить доступ объекту игрока, запустившего команду), а второе - от ServerEvent, который объединяет все общие события самого сервера:

      /******************************************************************************** * обработка события выполнения команды, отправленной игроком********************************************************************************/@EventHandlerpublic void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event){    String cmd_line= event[paste][/paste]getMessage();    [paste][/paste][paste][/paste][paste][/paste]}/******************************************************************************** * обработка события выполнения команды, введенной с консоли********************************************************************************/@EventHandlerpublic void onServerCommandPreprocess(ServerCommandEvent event){    String cmd_line= event[paste][/paste]getCommand();    [paste][/paste][paste][/paste][paste][/paste]}

      Оба типа событий наследуют интерфейс Cancellable, что позволяет в особо запущенных случаях произвести отмену выполнения какой-либо команды. Например, создав обработчик события PlayerCommandPreprocessEvent с приоритетом MONITOR (см. статью Не думай о событиях свысока!) для обработки единственной команды /seed с единственным действием - отменой введенной команды, можно полностью перекрыть к ней доступ даже для операторов сервера. Сид мира - это святое.

      Однако главное отличие данного метода (через обработку событий) в другом: в отличие от метода onCommand они НЕ ТРЕБУЮТ описания (или регистрации, см далее) используемых команд в файле plugin.yml плагина. То есть (теоретически), вы можете через обработчик события перехватить ЛЮБУЮ команду любого плагина. "Теоретичность" заключается в том, что ваш плагин для этого должен в очереди на обработку события оказаться впереди плагина, чью команду вы хотите перехватить. Достигается это, как правило, повышением уровня приоритета обработчика события. Конечно, если плагин, чью команду вы перехватили, зарегистрировал обработчик команд с опцией ignoreCancelled= true, это вас не спасет. Хотя и тут можно нарезать левую резьбу: заменить название команды на что-нибудь типа /dummy и отдельным обработчиком с наименьшим приоритетом "погасить" ее в самом конце "очереди".

      Из преимущества, которое не требует предварительной регистрации команды, проистекает главный недостаток данного метода: для команды не создается (да и не может) объект типа Command, в котором перечислены все ее основные характеристики. Команда поступает на обработку именно в том виде, в каком она была введена в консоли сервера (ServerCommandEvent) или в чате (PlayerCommandPreprocessEvent). Однако и тут имеются различия: для ServerCommandEvent эта строка получается вызовом event.getCommand(), а для PlayerCommandPreprocessEvent - через вызов event.getMessage() (шизофрения прогрессирует). Причем во втором случае, обратите внимание, полученная строка команды будет начинаться со слеша ("/").

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

      Кроме того, отсутствие явной регистрации команды приводит к еще одному серьезному недостатку: при вводе части команды пользователь может нажать клавишу Tab и получить контекстную подсказку о подходящих командах. Если команда не будет явно зарегистрирована в системе, логично, что она не войдет в этот т.н. таб-комплит (TabComplete) список. Что же делать, если команд у вашего плагина набралось уже порядочно, структуру они имеют довольно сложную, и к тому же активно допиливаются, что превращает их синхронизацию с описанием в plugin.yml в сущий кошмар? Тогда, наверное, будет проще организовать их декларацию в коде плагина (к чему я, собственно, и пришел в своих плагинах) - вместе с возможными наборами аргументов, их типами, допустимыми опциями и их перекрестными зависимостями. А регистрацию команд производить программно - при запуске плагина (в методе onEnable).

      К сожалению, подобная схема регистрации команд требует особых низкоуровневых методов ведения допроса доступа к закрытым полям объектов сервера, а их мы еще не проходили. Обещаю, что как только мы рассмотрим тему "рефлексных методов", первым делом опишу как раз способ программной регистрации команд в системе. Для знакомых с данными методами даю небольшую подсказку: метод public boolean register(String label, String fallbackPrefix, Command command) объекта private final SimpleCommandMap commandMap менеджера плагинов вашего сервера.

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


  • Поблагодарили: Hedgehog1024, avttrue и Ovan



Темы с аналогичным тегами bukkit, plugins, команды

Количество пользователей, читающих эту тему: 0

0 пользователей, 0 гостей, 0 анонимных