Создание команд позволяет разработчику наращивать функционал и способ воздействия с пользователем через, собственно, команды. Это руководство расскажет как регистрировать команды и общую информацию о структуре Brigadier.
Примечание: Весь код, описанный здесь, написан для 1.14.4. Что-то может быть изменено в yarn, но большая часть кода всё ещё применима и там.
Brigadier - исполнитель и обработчик команд, написаный Mojang для Minecraft. Brigadier это древоподобная библиотека, с помощью которой вы можете создавать дерево из команд и аргументов.
С исходным кодом Brigadier вы можете ознакомиться тут: https://github.com/Mojang/brigadier
Brigadier требует описание команды
чтобы её исполнять. На самом деле, “команда” это довольно неточный термин, если мы говорим о Brigadier, но обычно он означает конечную точку для команды, т.е. то место, где исполняется ваш код.
Command
- это функциональный интерфейс. Команда имеет подтип S
, где S
- это источник команды. Источник команды может предоставить некий “контекст” для неё. В Minecraft обычно используется ServerCommandSource
, что представляет сервер, командный блок, соединение RCON, игрока или сущность.
Единственный метод в Command
- run(CommandContext<S>)
принимает один параметр CommandContext<S>
и возвращает целое число. Контекст команды (CommandContext) типа S
содержит в себе S
, позволяет увидеть используемые аргументы, работать с обработанными аргументами и посмотреть на непосредственный ввод пользователя.
Вы можете создать команду несколькими образами, которые написаны ниже:
Как лямбда-выражение
Command<Object> command = context -> { return 0; };
Как анонимный класс
Command<Object> command = new Command<Object>() { @Override public int run(CommandContext<Object> context) { return 0; } }
Как часть класса
final class XYZCommand implements Command<Object> { @Override public int run(CommandContext<Object> context) { return 0; } }
Как обращение к методу
void registerCommand() { // Не обращайте внимание, мы объясним про это далее. dispatcher.register(CommandManager.literal("foo")) .executes(this::execute); // Это относится к приведённому ниже методу "выполнить". } private int execute(CommandContext<Object> context) { return 0; }
Метод run(CommandContext)
может произвести исключение CommandSyntaxException
, это будет объяснено далее в руководстве.
Возвращаемое число можно понимать как результат команды. В Minecraft результатом может являться силой сигнала красного камня, который выдаёт командный блок или число, которое будет передано в следующий цепной командный блок. Обычно отрицательное число значит что команда не выполнена и что-то пошло не по плану, 0
что команда прошла, положительные числа - что выполнена и что-то произошло.
Ниже вы можете увидеть команду, без аргументов:
dispatcher.register(CommandManager.literal("foo").executes(context -> { return 1; }));
CommandManager.literal(“foo”)
даёт brigadier'у понять, что у команды только одна ветвь, это буквальный ввод foo. Чтобы команда была выполнена, нужно ввести /foo
. Если введено /Foo
, /FoO
, /FOO
, /fOO
или /fooo
, команда не будет считаться верной.
Вы можете вводить CommandManager.literal(“foo”)
каждый раз, когда хотите создать литерал. Это работает, но вы можете статически импортировать аргументы и сократить оператор до literal(“foo”)
. Это также работает для получения значения аргумента. Это сокращает StringArgumentType.getString(ctx, “string”)
до getString(ctx, “string”)
.
Это также работает для собственных типов аргументов Minecraft.
И ваш импорт будет выглядеть примерно так:
// getString(ctx, "string") import static com.mojang.brigadier.arguments.StringArgumentType.getString; // word() import static com.mojang.brigadier.arguments.StringArgumentType.word; // literal("foo") import static net.minecraft.server.command.CommandManager.literal; // argument("bar", word()) import static net.minecraft.server.command.CommandManager.argument; // Импортировать всё import static net.minecraft.server.command.CommandManager.*;
Примечание: Пожалуйста, убедитесь, что вы используете literal
и argument
из CommandManager, иначе у вас могут возникнуть проблемы с дженериками при попытке компиляции.
Аргументы Brigadier по умолчанию находятся в com.mojang.brigadier.arguments
Аргументы Minecraft находятся в разделе net.minecraft.command.arguments
.
CommandManager находится в net.minecraft.server.command
Чтобы добавить команде подкоманду, сначала необходимо зарегистрировать ветвь с буквальным вводом.
dispatcher.register(CommandManager.literal("foo")
В целом, чтобы получить подкоманду, нужно расширить текущую ветвь следующей, уже существующей ветвью. Это можно сделать с помощью метода then(ArgumentBuilder)
который принимает в себя ArgumentBuilder
.
То, что показано ниже, создаёт команду foo <bar>
:
dispatcher.register(CommandManager.literal("foo") .then(CommandManager.literal("bar")) );
Рекомендуется делать отступы в вашем коде при добавлении узлов в команду. Обычно отступ соответствует количеству глубоких узлов в дереве команд. Новая строка также делает видимым, что добавляется еще один узел. Существуют альтернативные стили для форматирования команды дерева, которые будут показаны позже в этом руководстве.
Итак, давайте попробуем выполнить команду
Скорее всего, если вы наберете /foo bar
в игре, команда не будет выполнена. Это происходит потому, что нет кода для игры, который выполнялся бы после выполнения всех требуемых аргументов. Чтобы исправить это, вам нужно указать игре, что запускать, когда выполняется команда, используя метод executes(Command)
. Ниже в качестве примера показано, как должна выглядеть команда:
dispatcher.register(CommandManager.literal("foo") .then(CommandManager.literal("bar") .executes(context -> { return 1; }) ) );
Регистрация команд выполняется путем регистрации обратного вызова с помощью CommandRegistrationCallback
. Для получения информации о регистрации обратных вызовов, ознакомьтесь с статьёй об обратных вызовах.
Событие должно быть зарегистрировано в инициализаторе вашего мода. Обратный вызов имеет два параметра. CommmandDispatcher<S>
используется для регистрации, анализа и выполнения команд. S
i- это тип источника команд, который поддерживает диспетчер команд. Второй параметр - это логическое значение, которое определяет тип сервера, на котором регистрируются команды. Это может быть dedicated
(выделенный) или integrated
(интегрированный) (false) сервер:
public class ExampleCommandMod implements ModInitializer { @Override public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { ... }); } }
Внутри вашего лямбда-выражения, ссылки на метод или чего бы вы ни выбрали, зарегистрируете свои команды:
public class ExampleCommandMod implements ModInitializer { @Override public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { dispatcher.register(CommandManager.literal("foo").executes(context -> { return 1; })); }); } }
При желании вы можете убедиться, что команда зарегистрирована только на выделенном сервере, установив флажок dedicated
(выделенный):
public class ExampleCommandMod implements ModInitializer { @Override public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { if (dedicated) { TestDedicatedCommand.register(dispatcher); } }); } }
И наоборот:
public class ExampleCommandMod implements ModInitializer { @Override public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { if (!dedicated) { TestIntegratedCommand.register(dispatcher); } }); } }
Аргументы в Brigadier как анализируют, так и проверяют на ошибки любые введенные аргументы.
Minecraft создает некоторые специальные типы аргументов для собственного использования, такие как EntityArgumentType
, который представляет внутриигровые селекторы сущностей @a, @r, @p, @e[type=!player, limit=1, distance=..2]
, или NbtTagArgumentType
, который анализирует строковый nbt (snbt) и проверяет правильность ввода синтаксиса.
Будет сделано позже: Подробнее о том, как использовать аргументы.
Ниже приведены ссылки на статьи о более сложных концепциях, используемых в Brigadier.
Страница | Описание |
---|---|
Условия | Разрешать пользователям выполнять команды только в определенных сценариях. |
Исключения | Сбой выполнения команды с описательным сообщением и в определенных контекстах. |
Предложения | Предложение входных данных для отправки клиенту. |
Редиректы (Разные вариации) | Разрешить использование разновидностей команд. |
Редиректы (Цепочки) | Разрешить командам иметь повторяющиеся элементы и флаги. |
Свои типы аргументов | Анализируйте свои аргументы и возвращайте свои типы. |
Есть две непосредственные возможности того, почему это могло произойти.
Решение этой проблемы состоит в том, чтобы заставить методы run или suggest вызывать исключение CommandSyntaxException. Не волнуйтесь, Brigadier обработает исключения и выдаст соответствующее сообщение об ошибке.
Время от времени у вас могут возникать проблемы с типами дженерика. Убедитесь, что вы используете CommandManager.literal(…)
или CommandManager.argument(…)
вместо LiteralArgumentBuilder
или RequiredArgumentBuilder
.
Fabric в настоящее время не поддерживает команды на стороне клиента. Существует сторонний мод от команды Cotton, которая добавляет эту функциональность.
Несколько вещей, которые мы не рекомендуем, но которые возможны.
Вы можете это сделать, но это не рекомендуется. Вы получите CommandManager
с сервера и добавите любые команды, которые пожелаете, в его CommandDispatcher
.
После этого вам нужно снова отправить дерево команд каждому игроку, используя sendCommandTree(ServerPlayerEntity)
. Это необходимо, поскольку клиент локально кэширует дерево команд, которое он получает во время входа в систему (или при отправке пакетов оператора) для локальных завершений и сообщений об ошибках.
Вы также можете сделать это, однако это гораздо менее стабильно, чем регистрация команд, и может вызвать нежелательные побочные эффекты. Чтобы все было просто, вам нужно использовать отражение для бригадира и удалить узлы. После этого вам нужно снова отправить дерево команд каждому игроку, используя sendCommandTree(ServerPlayerEntity)
. Если вы не отправите обновленное дерево команд, клиент может подумать, что команда все еще существует, даже если сервер завершит выполнение с ошибкой.
ServerCommandSource
предоставляет некоторый дополнительный контекст, специфичный для конкретной реализации, при выполнении команды. Это включает в себя возможность получить объект, который выполнил команду, мир, в котором была запущена команда, или сервер, на котором была запущена команда:
final ServerCommandSource source = ctx.getSource(); // Находка источника. Это всегда будет работать. // Непроверенный, может быть нулевым, если отправителем была консоль. // Завершит выполнение команды, если источником команды не была не сущность. // В результате этого может появиться игрок. Также будет отправлена обратная связь, сообщающая отправителю команды, что он должен быть сущностью. // Этот метод потребует, чтобы ваши методы вызывали исключение CommandSyntaxException. // Параметры сущности в ServerCommandSource могут возвращать сущность CommandBlock, любое живое существо или игрока. final ServerPlayerEntity player = source.getPlayer(); // Завершит выполнение команды, если источником команды явно не был игрок. Также будет отправлен отзыв, сообщающий отправителю команды, что он должен быть игроком. // Этот метод потребует, чтобы ваши методы вызывали исключение CommandSyntaxException source.getPosition(); // Позиция отправителя в качестве Vec3, когда была отправлена команда. Это может быть местоположение сущности/командного блока или, в случае консоли, точка появления в мире. source.getWorld(); // Мир, в котором находится отправитель. Мир консоли такой же, как и мир создания по умолчанию. source.getRotation(); // Вращение отправителя как Vec2f. source.getMinecraftServer(); // Доступ к экземпляру MinecraftServer, на котором была запущена эта команда. source.getName(); // Имя источника команды. Это может быть имя сущности, игрока, имя командного блока, который был переименован перед размещением, или, в случае консоли, "Console". source.hasPermissionLevel(int level); // Возвращает значение true, если источник команды имеет определенный уровень разрешений. Это зависит от статуса оператора отправителя. (На интегрированном сервере у игрока должны быть включены читы для выполнения этих команд)
public static void register(CommandDispatcher<ServerCommandSource> dispatcher){ dispatcher.register(literal("broadcast") .requires(source -> source.hasPermissionLevel(2)) // Должен быть мастером игры, чтобы использовать эту команду. Команда не будет отображаться при перебора через Tab или выполняться не для операторов или любого оператора, имеющего уровень разрешений 1. .then(argument("color", ColorArgumentType.color()) .then(argument("message", greedyString()) .executes(ctx -> broadcast(ctx.getSource(), getColor(ctx, "color"), getString(ctx, "message")))))); // Вы можете разобраться с приведенными здесь аргументами и передать их в команду. } final Text text = new LiteralText(message).formatting(formatting); source.getMinecraftServer().getPlayerManager().broadcastChatMessage(text, false); return Command.SINGLE_SUCCESS; // Успех }
Сначала базовый код, в котором мы регистрируем giveMeDiamond
как литерал, а затем вызываем блок, чтобы сообщить диспетчеру, какой метод запускать:
public static LiteralCommandNode register(CommandDispatcher<ServerCommandSource> dispatcher) { // Вы также можете вернуть LiteralCommandNode для использования с возможными редиректорами. return dispatcher.register(literal("giveMeDiamond") .executes(ctx -> giveDiamond(ctx))); }
Затем, поскольку мы хотим передавать только игрокам, мы проверяем, является ли CommandSource игроком. Но мы можем использовать getPlayer
и сделать и то, и другое одновременно, и выдать ошибку, если источник не является игроком:
public static int giveDiamond(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { final ServerCommandSource source = ctx.getSource(); final PlayerEntity self = source.getPlayer(); // Если это не игрок, то команда заканчивается
Затем мы добавляем в инвентарь игрока, проверяя, заполнен ли инвентарь:
if(!player.inventory.insertStack(new ItemStack(Items.DIAMOND))){ throw new SimpleCommandExceptionType(new TranslatableText("inventory.isfull")).create(); } return 1; }
…брось ты свою Святую Ручную Гранату из Антиохии своему врагу. который, будучи непослушным в моих глазах, погубит его. (Иностранный прикол)
Помимо шутки, эта команда вызывает заряженный TNT в указанное местоположение или местоположение курсора отправителя.
Сначала создайте запись в CommandDispatcher, которая принимает литерал antioch с необязательным аргументом местоположения для вызова сущности.
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) { dispatcher.register(literal("antioch") .then(required("location", BlockPosArgumentType.blockPos() .executes(ctx -> antioch(ctx.getSource(), BlockPosArgument.getBlockPos(ctx, "location"))))) .executes(ctx -> antioch(ctx.getSource(), null))); }
Затем создайте сообщения, стоящие за шуткой:
public static int antioch(ServerCommandSource source, BlockPos blockPos) throws CommandSyntaxException { if(blockPos == null) { // В случае отсутствия введенного аргумента мы вычисляем положение курсора игрока или выдаем ошибку, если ближайшая позиция находится слишком далеко или находится за пределами мира. // Этот класс используется в качестве примера и на самом деле еще не существует. blockPos = LocationUtil.calculateCursorOrThrow(source, source.getRotation()); } final TntEntity tnt = new TntEntity(source.getWorld(), blockPos.getX(), blockPos.getY(), blockPos.getZ(), null); tnt.setFuse(3); source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("...брось ты свою Святую Ручную Гранату из Антиохии своему врагу").formatting(Formatting.RED), false); source.getMinecraftServer().getPlayerManager().broadcastChatMessage(new LiteralText("который, будучи непослушным в моих глазах, погубит его.").formatting(Formatting.RED), false); source.getWorld().spawnEntity(tnt); return 1; }