Обработка команд игроков

Что такое команды игроков?

Команда игрока — это команда, которую игрок вводит в чате, чтобы выполнить какое-то особое действие. Это могут быть и развлекательные пользовательские механики, и инструменты модерации — такие команды встречаются в Minecraft повсюду. Denizen позволяет создавать собственные команды игроков, чтобы расширить возможности вашего сервера. На этой странице мы шаг за шагом сделаем простую команду «warp» — команду, которая позволяет игроку создавать точки, на которые потом смогут телепортироваться другие. Это иллюстрация того, как делать кастомные команды игроков, и заодно — обзор разных вещей, о которых стоит подумать, когда такие команды создаёшь.

Базовая структура

Для начала: все кастомные команды в Denizen используют одну и ту же базовую структуру. Она похожа на те task-скрипты, которые вы уже видели, но с парой дополнительных обязательных полей:

ScriptContainerName:
    type: command
    name: [commandName]
    description: [a short description of your command]
    usage: [a description of the usage]
    permission: [permission key]
    script:
    - (commands here)

В редакторе скриптов можно просто набрать command и через tab-дополнение в любой момент сгенерировать шаблон command-скрипта.

Помимо указания типа скрипта — command, — нужно также задать имя команды. Именно его пользователь будет вводить в чате, чтобы вызвать команду (без префикса /).

Также нужно указать описание и usage-текст. Они в основном нужны для стандартной команды /help.

Ещё, как правило, стоит задать permission-ноду — пока это может быть любой выдуманный ключ. Скорее всего, на тестовом сервере у вас есть op-права, и проверка прав всё равно будет обойдена.

Итак, начнём command-скрипт с заполнения обязательных полей:

warp_command:
    type: command
    name: warp
    description: Warps you to magical places.
    usage: /warp
    permission: dscript.warp
    script:
    - narrate "<&[base]>This is where my command will go!"

Попробуйте прямо в игре: выполните /warp. В чате просто появится This is where my command will go!. До конечной цели ещё далеко, но наша кастомная команда уже хоть что-то делает.

../../_images/warp_command_example_1.png

Обработка аргументов игрока

Часто командой дело не ограничивается — обычно игроку нужно передать ещё какие-то подробности. В нашем случае с командой warp мы хотим и создавать warp-точки, и телепортироваться на них. Например, я мог бы использовать /warp create spawn, чтобы создать новую warp-точку с именем spawn. В этом случае create и spawn — это аргументы.

Как и в событиях, у command-скрипта есть несколько тегов context, в которых хранится информация о том, как именно игрок вызвал команду. Один из них — <context.args>, содержащий список всех переданных игроком аргументов.

Добавим нашей команде функциональности: пусть она создаёт новые warp-точки и позволяет на них телепортироваться:

warp_command:
    type: command
    name: warp
    description: Warps you to magical places.
    usage: /warp create|goto [warp-name]
    permission: dscript.warp
    script:
    - choose <context.args.first>:
        - case create:
            - define name <context.args.get[2]>
            - flag server warps.<[name]>:<player.location>
            - narrate "<&[base]>Created warp <&[emphasis]><[name]><&[base]>!"
        - case goto:
            - define name <context.args.get[2]>
            - teleport <player> <server.flag[warps.<[name]>]>
            - narrate "<&[base]>Warped to <&[emphasis]><[name]><&[base]>!"

Проверьте: введите /warp create test, затем отойдите немного в сторону, а потом — /warp goto test. Вы окажетесь в той точке, в которой задали warp.

Что тут происходит:

  • Смотрим на первый переданный аргумент

  • Если он равен create — создаём на сервере флаг, имя которого берём из второго аргумента

  • Иначе, если первый аргумент — goto, телепортируем игрока в точку, хранящуюся во флаге, чьё имя указано вторым аргументом

Наверняка вы уже заметили: если игрок введёт команду как-то иначе, скрипт сломается. Как с этим справляться, разберём чуть ниже.

Обратите внимание: мы заодно обновили текст usage. На саму работу скрипта это не влияет, но документацию лучше держать актуальной.

Ещё вы наверняка заметили, что команда narrate использует несколько цветов (из стандартного набора, заданного в config.yml Denizen), чтобы ключевые части сообщения визуально выделялись. Такие мелочи сильно помогают сделать команды дружелюбнее к игрокам.

Аргумент-имя другого игрока

У многих команд есть возможность подействовать на другого игрока, и тогда нужно передавать его имя. Например, в нашей warp-команде мы можем захотеть телепортировать не только себя, но и другого игрока.

Допустим, мы хотим команду вида /warp player (player name) (destination). Тогда скрипт будет выглядеть примерно так:

warp_command:
    type: command
    name: warp
    description: Warps you to magical places.
    usage: /warp create [warp-name] | goto [warp-name] | player [player] [warp-name]
    permission: dscript.warp
    script:
    - choose <context.args.first>:
        - case create:
            - define name <context.args.get[2]>
            - flag server warps.<[name]>:<player.location>
            - narrate "<&[base]>Created warp <&[emphasis]><[name]><&[base]>!"
        - case goto:
            - define name <context.args.get[2]>
            - teleport <player> <server.flag[warps.<[name]>]>
            - narrate "<&[base]>Warped to <&[emphasis]><[name]><&[base]>!"
        - case player:
            - define playerName <context.args.get[2]>
            - define destination <context.args.get[3]>
            - define playerToWarp <server.match_player[<[playerName]>]>
            - teleport <[playerToWarp]> <server.flag[warps.<[destination]>]>
            - narrate "<&[base]>Warped <&[emphasis]><[playerToWarp].name> <&[base]>to <&[emphasis]><[destination]><&[base]>!"

Попробуйте применить это к другому игроку на сервере, а если играете на локалхосте — просто передайте собственное имя.

Обратите внимание: мы не используем переданный аргумент напрямую в команде телепортации, а применяем тег <server.match_player[]>, чтобы получить объект игрока по его имени. Это важный шаг: в Denizen есть «умные» конвертеры, которые сами подставляют нужный объект по имени (например, для материалов), но для игроков так не работает. К тому же <server.match_player[]> понимает неточные имена — например, если на сервере есть игрок bobby, то <server.match_player[bob]> сработает (если, конечно, на сервере нет игрока с именем bob).

../../_images/warp_command_example_2.png

Добавляем tab-дополнение

Отличный способ прокачать свои command-скрипты — добавить tab-дополнение, встроенную в Minecraft систему автодополнения аргументов. Чтобы это сделать, достаточно добавить вот такой блок:

warp_command:
    type: command
    name: warp
    description: Warps you to magical places.
    usage: /warp create [warp-name] | goto [warp-name] | player [player] [warp-name]
    permission: dscript.warp
    tab completions:
        1: create|goto|player
        2: <context.args.first.equals[goto].if_true[<server.flag[warps].keys>].if_false[]>
        3: <context.args.first.equals[player].if_true[<server.flag[warps].keys>].if_false[]>
    script:
    #...nothing changed here

Попробуйте: пока набираете команду, Minecraft покажет меню tab-дополнения — увидите всё своими глазами.

Что произойдёт: пока игрок вводит первый аргумент, будут показаны подсказки create, goto и player, а наиболее подходящая по вводу — подсвечена.

Когда игрок вводит второй аргумент, если первый аргумент — goto, берутся все ключи из флага warps; иначе подсказок не будет. То же самое делается и для третьего аргумента, только там проверяется, что первый аргумент — player.

../../_images/warp_command_tabcomplete.png

Имейте в виду: tab-дополнение само по себе не добавляет скрипту никакой логики — оно лишь подсказывает значения игроку. Обработку любых введённых аргументов всё равно нужно прописывать в скрипте.

Наверняка вы уже заметили: tab-дополнение для имён игроков в подкоманде «player» потребовало бы очень длинной строки для второго аргумента. Когда вы упираетесь в подобное — это первый знак, что команда становится слишком сложной, и её стоит разбить на несколько (например, command-скрипт createwarp [name], отдельный от command-скрипта warpto [name] (player)).

Не доверяйте игроку

В текущем виде скрипт формально работает — при условии, что все пользуются им правильно. Но нет никакой гарантии, что так и будет. Здесь мы подходим к важной теме: никогда нельзя доверять игроку. Не стоит рассчитывать на то, что игрок сделает именно то, что вы хотите или ожидаете.

Кто-то попытается сжульничать или эксплуатировать систему, кто-то просто ошибётся. Люди не идеальны — а значит, ваш command-скрипт должен быть к этому готов.

С нашей простой командой связан целый ряд проблем, например:

  • Что если кто-то попробует телепортироваться на несуществующий warp?

  • Что если кто-то попробует создать warp с уже занятым именем?

  • Что если игрок забудет указать аргументы?

  • Что если игрок укажет слишком много аргументов?

  • Что если игрок передаст неверный аргумент?

  • Что если указанного игрока не существует?

  • Что если в имени warp-точки окажется «несовместимый» символ — например, ., пробел или что-то ещё?

../../_images/warp_command_errors.png

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

Посмотрим, как выглядит наш скрипт с тщательной проверкой ошибок:

warp_command:
    type: command
    name: warp
    description: Warps you to magical places.
    usage: /warp create [warp-name] | goto [warp-name] | player [player] [warp-name]
    permission: dscript.warp
    tab completions:
        1: create|goto|player
        2: <context.args.first.equals[goto].if_true[<server.flag[warps].keys.parse[unescaped].if_null[]>].if_false[]>
        3: <context.args.first.equals[player].if_true[<server.flag[warps].keys.parse[unescaped].if_null[]>].if_false[]>
    script:
    # If the command is used without input, the player probably doesn't know what to do
    # So tell them
    - if <context.args.is_empty>:
        - narrate "<&[error]>/warp create [warp-name]"
        - narrate "<&[error]>/warp goto [warp-name]"
        - narrate "<&[error]>/warp player [player] [warp-name]"
        - stop
    - choose <context.args.first>:
        - case create:
            # Validate the argument count
            - if <context.args.size> < 2:
                - narrate "<&[error]>You need to give the warp a name!"
                - stop
            - else if <context.args.size> > 2:
                - narrate "<&[error]>You provided too many arguments!"
                - stop
            - define name <context.args.get[2]>
            # Don't let users overwrite existing warps
            # Use ".escaped" to prevent strange symbols in the name causing issues
            - if <server.has_flag[warps.<[name].escaped>]>:
                - narrate "<&[error]>A warp already exists with the name of <&[emphasis]><[name]><&[error]>!"
                - stop
            # This means you'll need a 'warp delete [name]' as well
            - flag server warps.<[name].escaped>:<player.location>
            - narrate "<&[base]>Created warp <&[emphasis]><[name]><&[base]>!"
        - case goto:
            - if <context.args.size> < 2:
                - narrate "<&[error]>You need to specify where to warp to!"
                - stop
            - else if <context.args.size> > 2:
                - narrate "<&[error]>You provided too many arguments!"
                - stop
            - define name <context.args.get[2]>
            # Make sure the warp exists
            - if !<server.has_flag[warps.<[name].escaped>]>:
                - narrate "<&[error]>No warp by <&[emphasis]><[name]> <&[error]>exists!"
                - stop
            - teleport <player> <server.flag[warps.<[name].escaped>]>
            - narrate "<&[base]>Warped to <&[emphasis]><[name]><&[base]>!"
        - case player:
            - if <context.args.size> < 3:
                - narrate "<&[error]>You need to specify a player and where to warp to!"
                - stop
            - else if <context.args.size> > 3:
                - narrate "<&[error]>You provided too many arguments!"
                - stop
            - define playerName <context.args.get[2]>
            - define destination <context.args.get[3]>
            # Use a fallback to catch invalid player name input
            - define playerToWarp <server.match_player[<[playerName]>].if_null[null]>
            - if <[playerToWarp]> == null:
                - narrate "<&[error]>Can't find player by name '<&[emphasis]><[playerName]><&[error]>'!"
                - stop
            - if !<server.has_flag[warps.<[destination].escaped>]>:
                - narrate "<&[error]>No warp by <&[emphasis]><[destination]> <&[error]>exists!"
                - stop
            - teleport <[playerToWarp]> <server.flag[warps.<[destination].escaped>]>
            - narrate "<&[base]>Warped <&[emphasis]><[playerToWarp].name> <&[base]>to <&[emphasis]><[destination]><&[base]>!"
        # Handle invalid first-argument with a simple error message to the player
        - default:
            - narrate "<&[error]>Unknown argument '<&[emphasis]><context.args.first><&[error]>'!"

Попробуйте теперь специально передавать некорректные значения и посмотрите, как команда останавливает вас.

Несколько применённых здесь защитных приёмов:

  • Проверка размера списка аргументов, чтобы убедиться, что нужный аргумент действительно введён

  • Проверка наличия флага на сервере, чтобы не было конфликтов

  • Остановка скрипта при обнаружении любой проблемы и сообщение игроку, в чём она состоит

  • Null-проверка в tab-дополнениях, чтобы при отсутствии warps Denizen не сыпал ошибками

  • Null-проверка в теге match_player, чтобы убедиться, что игрок реально существует

  • Использование тега .escaped, чтобы в имя флага не попали «ломающие» синтаксис символы вроде .

Хотя скрипт заметно вырос, это необходимый шаг для команд и для пользовательских интерфейсов в целом. Подробнее об этом — на странице «Распространённые ошибки».

../../_images/warp_command_safe.png

Кстати, а можно посмотреть ваши права?

Помимо проверки корректности введённых игроком аргументов, нужно ещё удостовериться, что у него есть право пользоваться командой. По умолчанию в Minecraft нет системы прав (кроме выдачи op-статуса), но существует множество плагинов прав, помогающих с этим, — если у вас уже запущен свой сервер, скорее всего, один из них уже установлен.

Первый уровень проверки прав — есть ли у игрока вообще доступ к команде. За это отвечает строка permission: dscript.warp, которую мы добавляли во все примеры. Благодаря ей любой игрок без op-статуса и без права dscript.warp не сможет пользоваться командой. Вообще, добавлять такую permission-ноду к любой кастомной команде — хорошая практика, даже если вы предполагаете, что командой будут пользоваться все.

Имя permission-ключа может быть любым: вы создаёте новую ноду, и имя выбираете сами.

Обычно используют простой формат вроде dscript.[CommandName] или [ProjectName].[CommandName] — старайтесь, чтобы имена нод были понятными.

../../_images/warp_command_noperms.png

Иногда хочется проверять не только базовый доступ к команде, но и более тонкие вещи. Например, пусть любой игрок может телепортироваться по warp-точкам, но создавать новые и принудительно телепортировать других — не может. Добавить такую проверку прав можно, например, так:

warp_command:
    type: command
    name: warp
    description: Warps you to magical places.
    usage: /warp create [warp-name] | goto [warp-name] | player [player] [warp-name]
    permission: dscript.warp
    tab completions:
        1: create|goto|player
        2: <context.args.first.equals[goto].if_true[<server.flag[warps].keys.parse[unescaped].if_null[]>].if_false[]>
        3: <context.args.first.equals[player].if_true[<server.flag[warps].keys.parse[unescaped].if_null[]>].if_false[]>
    script:
    - if <context.args.is_empty>:
        - narrate "<&[error]>/warp create [warp-name]"
        - narrate "<&[error]>/warp goto [warp-name]"
        - narrate "<&[error]>/warp player [player] [warp-name]"
        - stop
    - choose <context.args.first>:
        - case create:
            #+ Require a special warp creation permission
            - if !<player.has_permission[dscript.warp.create]>:
                - narrate "<&[error]>You don't have permission to use this!"
                - stop
            - if <context.args.size> < 2:
                - narrate "<&[error]>You need to give the warp a name!"
                - stop
            - else if <context.args.size> > 2:
                - narrate "<&[error]>You provided too many arguments!"
                - stop
            - define name <context.args.get[2]>
            - if <server.has_flag[warps.<[name].escaped>]>:
                - narrate "<&[error]>A warp already exists with the name of <&[emphasis]><[name]><&[error]>!"
                - stop
            - flag server warps.<[name].escaped>:<player.location>
            - narrate "<&[base]>Created warp <&[emphasis]><[name]><&[base]>!"
        - case goto:
            - if <context.args.size> < 2:
                - narrate "<&[error]>You need to specify where to warp to!"
                - stop
            - else if <context.args.size> > 2:
                - narrate "<&[error]>You provided too many arguments!"
                - stop
            - define name <context.args.get[2]>
            - if !<server.has_flag[warps.<[name].escaped>]>:
                - narrate "<&[error]>No warp by <&[emphasis]><[name]> <&[error]>exists!"
                - stop
            - teleport <player> <server.flag[warps.<[name].escaped>]>
            - narrate "<&[base]>Warped to <&[emphasis]><[name]><&[base]>!"
        - case player:
            #+ Require a special warp-other-player permission
            - if !<player.has_permission[dscript.warp.other]>:
                - narrate "<&[error]>You don't have permission to use this!"
                - stop
            - if <context.args.size> < 3:
                - narrate "<&[error]>You need to specify a player and where to warp to!"
                - stop
            - else if <context.args.size> > 3:
                - narrate "<&[error]>You provided too many arguments!"
                - stop
            - define playerName <context.args.get[2]>
            - define destination <context.args.get[3]>
            # Use a fallback to catch invalid player name input
            - define playerToWarp <server.match_player[<[playerName]>].if_null[null]>
            - if <[playerToWarp]> == null:
                - narrate "<&[error]>Can't find player by name '<&[emphasis]><[playerName]><&[error]>'!"
                - stop
            - if !<server.has_flag[warps.<[destination].escaped>]>:
                - narrate "<&[error]>No warp by <&[emphasis]><[destination]> <&[error]>exists!"
                - stop
            - teleport <[playerToWarp]> <server.flag[warps.<[destination].escaped>]>
            - narrate "<&[base]>Warped <&[emphasis]><[playerToWarp].name> <&[base]>to <&[emphasis]><[destination]><&[base]>!"
        # Handle invalid first-argument with a simple error message to the player
        - default:
            - narrate "<&[error]>Unknown argument '<&[emphasis]><context.args.first><&[error]>'!"

Обратите внимание, что разрешения dscript.warp.create и dscript.warp.other — полностью выдуманные, и вам нужно будет дополнительно выдать их нужным игрокам (или группам) в конфигурации вашего плагина на разрешения (например, LuckPerms, GroupManager и т. п.).

Permissions (разрешения) — это большая отдельная тема. На большинстве серверов уже установлен плагин на разрешения, и базовая идея в том, чтобы на каждое конкретное действие, которое вы хотите разрешить или ограничить, заводить отдельный permission-узел. Примеры, показанные здесь, достаточны, чтобы начать работать с разрешениями в командах Denizen, но для более сложных настроек (например, ограничения по времени, по мирам и т. п.) обращайтесь к документации вашего плагина на разрешения.