Обработка команд игроков
Что такое команды игроков?
Команда игрока — это команда, которую игрок вводит в чате, чтобы выполнить какое-то особое действие. Это могут быть и развлекательные пользовательские механики, и инструменты модерации — такие команды встречаются в 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!. До конечной цели ещё далеко, но наша кастомная команда уже хоть что-то делает.

Обработка аргументов игрока
Часто командой дело не ограничивается — обычно игроку нужно передать ещё какие-то подробности. В нашем случае с командой 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).

Добавляем 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.

Имейте в виду: tab-дополнение само по себе не добавляет скрипту никакой логики — оно лишь подсказывает значения игроку. Обработку любых введённых аргументов всё равно нужно прописывать в скрипте.
Наверняка вы уже заметили: tab-дополнение для имён игроков в подкоманде «player» потребовало бы очень длинной строки для второго аргумента. Когда вы упираетесь в подобное — это первый знак, что команда становится слишком сложной, и её стоит разбить на несколько (например, command-скрипт createwarp [name], отдельный от command-скрипта warpto [name] (player)).
Не доверяйте игроку
В текущем виде скрипт формально работает — при условии, что все пользуются им правильно. Но нет никакой гарантии, что так и будет. Здесь мы подходим к важной теме: никогда нельзя доверять игроку. Не стоит рассчитывать на то, что игрок сделает именно то, что вы хотите или ожидаете.
Кто-то попытается сжульничать или эксплуатировать систему, кто-то просто ошибётся. Люди не идеальны — а значит, ваш command-скрипт должен быть к этому готов.
С нашей простой командой связан целый ряд проблем, например:
Что если кто-то попробует телепортироваться на несуществующий warp?
Что если кто-то попробует создать warp с уже занятым именем?
Что если игрок забудет указать аргументы?
Что если игрок укажет слишком много аргументов?
Что если игрок передаст неверный аргумент?
Что если указанного игрока не существует?
Что если в имени warp-точки окажется «несовместимый» символ — например,
., пробел или что-то ещё?

Какие-то из этих случаев относительно безобидны (например, лишние аргументы), другие приводят к ошибкам в скрипте (например, отсутствующий 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, чтобы в имя флага не попали «ломающие» синтаксис символы вроде.
Хотя скрипт заметно вырос, это необходимый шаг для команд и для пользовательских интерфейсов в целом. Подробнее об этом — на странице «Распространённые ошибки».

Кстати, а можно посмотреть ваши права?
Помимо проверки корректности введённых игроком аргументов, нужно ещё удостовериться, что у него есть право пользоваться командой. По умолчанию в Minecraft нет системы прав (кроме выдачи op-статуса), но существует множество плагинов прав, помогающих с этим, — если у вас уже запущен свой сервер, скорее всего, один из них уже установлен.
Первый уровень проверки прав — есть ли у игрока вообще доступ к команде. За это отвечает строка permission: dscript.warp, которую мы добавляли во все примеры. Благодаря ей любой игрок без op-статуса и без права dscript.warp не сможет пользоваться командой. Вообще, добавлять такую permission-ноду к любой кастомной команде — хорошая практика, даже если вы предполагаете, что командой будут пользоваться все.
Имя permission-ключа может быть любым: вы создаёте новую ноду, и имя выбираете сами.
Обычно используют простой формат вроде dscript.[CommandName] или [ProjectName].[CommandName] — старайтесь, чтобы имена нод были понятными.

Иногда хочется проверять не только базовый доступ к команде, но и более тонкие вещи. Например, пусть любой игрок может телепортироваться по 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, но для более сложных настроек (например, ограничения по времени, по мирам и т. п.) обращайтесь к документации вашего плагина на разрешения.