Les commandes
-
En 1.13, Mojang a décidé de changer entièrement le système de commandes de Minecraft. Les développeurs (enfin, surtout Dinnerbone) ont ainsi créé Brigadier, une librairie open-source utilisée à présent dans Minecraft. Bien qu’elle ait été développée pour Minecraft, elle peut être utilisée dans bien d’autres programmes.
Dans ce tutoriel, nous allons donc nous pencher sur la création de commandes pour votre mod dans le cadre du modding Minecraft.Sommaire
Pré-requis
Concept
Avant de nous attaquer au code, on va d’abord voir comment les commandes sont vues dans Brigadier. En réalité, ce n’est pas très compliqué, c’est un simple arbre, chaque nœud de celui-ci peut être lié à aucun, un ou plusieurs autres nœuds. Quand on utilise une commande on ne fait que se déplacer dans cet arbre.
Le premier nœud de cet arbre est<root>
, lorsque que vous ajoutez une commande vous reliez en réalité un nouveau nœud à<root>
.
Voici un exemple de ce à quoi ressemble (partiellement) l’arbre représentant les commandes de Minecraft :Il existe plusieurs types de nœud, il y a les nœuds dits “littérales” et les nœuds de type “argument”. Un nœud littéral représente un texte prédéfini à taper, la plupart des nœuds montrés plus haut sont de ce type (ex.
add
,query
,say
,difficulty
, etc …). Les nœuds de type argument représentent une donnée qui sera entrée par l’utilisateur et dont on va se servir pour exécuter la commande. Par exemple, dans l’arbre ci-dessustime <int>
est un argument demandant un entier et la valeur passée sera utilisée dans l’exécution de la commande. De même quemessage <String>
qui est un message à afficher qui sera donné par l’utilisateur.Lorsque l’on tape une commande, on peut s’arrêter à n’importe quel nœud, vous pouvez alors rencontrer deux cas :
-
Le nœud a défini un comportement et dans ce cas celui-ci s’exécute. Par exemple, le nœud
difficulty
montré plus haut a pour comportement d’afficher la difficulté actuelle. -
Le nœud n’a pas défini de comportement et dans ce cas une erreur est affichée, c’est le cas par exemple du nœud
time
.
Bon, plongeons-nous à présent dans le code.
Code
Nous allons créer une commande qui met le feu aux entités renseignées dans la commande ou aux blocs dans le rayon indiqué. Pour cela, nous allons créer en premier lieu une classe
SetFireCommand
et dans cette classe nous allons ajouter une fonction prenant en paramètre un objetCommandDispatcher<CommandSource>
:public static void register(CommandDispatcher<CommandSource> dispatcher) { }
C’est grâce à cet objet
CommandDispatcher
que nous allons pouvoir enregistrer nos commandes.
Avant de commencer à coder notre commande, je vais vous donner la syntaxe que je souhaite pour ma commande.
Mettre le feu aux entités :/setfire entities <targets> [duration]
Mettre le feu aux blocs :/setfire blocks <radius>
Commençons !Pour enregistrer une commande, il faut appeler
CommandDispatcher#register
en lui passant unLiteralArgumentBuilder
. La chaîne de caractère passée dans ce builder sera le nom de notre commande (dans mon cas je met doncsetfire
).dispatcher.register( LiteralArgumentBuilder.<CommandSource>literal("setfire") // Il est nécessaire de renseigner le type générique );
Une chose que je n’ai pas précisé est que je veux que seuls les joueurs étant opérateurs puissent utiliser la commande, je vais donc l’indiquer tout de suite à l’aide de la méthode
ArgumentBuilder#requires
:dispatcher.register( LiteralArgumentBuilder.<CommandSource>literal("setfire") .requires(src -> src.hasPermissionLevel(2)) );
Pour rajouter un nouveau nœud au nœud
setfire
que nous venons de créer, il faut utiliser la méthodeArgumentBuilder#then
. Je vais commencer par le nœud concernant les entités :dispatcher.register( LiteralArgumentBuilder.<CommandSource>literal("setfire") .requires(src -> src.hasPermissionLevel(2)) .then( Commands.literal("entities") ) );
Je viens donc de créer un nœud littéral, si vous vous souvenez de la suite de la syntaxe de ma commande, je veux maintenant prendre en argument les entités à mettre en feu. Il faut donc que j’ajoute un nœud de type argument :
dispatcher.register( LiteralArgumentBuilder.<CommandSource>literal("setfire") .requires(src -> src.hasPermissionLevel(2)) .then( Commands.literal("entities") .then( Commands.argument("targets", EntityArgument.multipleEntities()) ) ) );
Pour ajouter un argument, on utilise donc
Commands#argument
qui demande le nom de l’argument (icitargets
car il représente les cibles de la commande), puis un type d’argument. Ici, j’ai utiliséEntityArgument.multipleEntities()
qui indique que j’accepte aucune, une seule ou plusieurs entités.Important
Il existe plusieurs types d’arguments, je ne vais pas les présenter toutes ici, vous retrouverez la plupart dans le reste du tutoriel.
Afin de continuer notre commande, nous allons créer une fonction dans notre classe qui aura pour but de mettre en feu une liste d’entités pendant une certaine durée :
/** * Met en feu les entités données pendant la durée donnée. Indique aussi au joueur que les entités ont été mises en * feu. * @param src La source de la commande * @param targets Les entités à mettre en feu * @param duration La durée du feu * @return le nombre d'entités mises en feu */ private static int setFireEntities(CommandSource src, Collection<? extends Entity> targets, int duration) { targets.forEach(e -> e.setFire(duration)); src.sendFeedback(new TextComponentString(targets.size() + " entities burnt !"), false); return targets.size(); }
Nous pourrons donc appeler cette fonction par la suite pour mettre en feu certaines entités. Continuons notre commande, nous sommes arrivés au nœud concernant les cibles. Il reste à renseigner la durée du feu, cependant, nous voulons que si celle-ci n’est pas renseignée les entités brûlent quand même pendant 5 secondes. Faisons cela :
Je ne mets ici qu’une partie du code de la commandeCommands.literal("entities") .then( Commands.argument("targets", EntityArgument.multipleEntities()) .executes(ctx -> setFireEntities(ctx.getSource(), EntityArgument.getEntitiesAllowingNone(ctx, "targets"), 5)) )
J’utilise
EntityArgument#getEntitiesAllowingNone
pour récupérer les cibles renseignées par l’utilisateur de la commande. J’y passe le contexte récupéré via la lambda ainsi que le nom de l’argument (icitargets
).Attention
Il est important que le nom passé dans
EntityArgument#getEntitiesAllowingNone
corresponde au nom donné dansCommands#argument
.Maintenant, si l’utilisateur renseigne la durée du feu il faut la prendre en compte. On rajoute donc un nœud à l’argument
targets
:Commands.argument("targets", EntityArgument.multipleEntities()) .executes(ctx -> setFireEntities(ctx.getSource(), EntityArgument.getEntitiesAllowingNone(ctx, "targets"), 5)) .then( Commands.argument("duration", IntegerArgumentType.integer(0)) )
Cette fois-ci, j’utilise le type
IntegerArgumentType
en appelantIntegerArgumentType#integer(int: min)
, le 0 correspond donc au minimum (je ne veux pas un temps négatif). Il faut à présent que je mette le feu aux entités pendant la durée renseignée.Commands.argument("duration", IntegerArgumentType.integer(0)) .executes(ctx -> setFireEntities(ctx.getSource(), EntityArgument.getEntitiesAllowingNone(ctx, "targets"), IntegerArgumentType.getInteger(ctx, "duration")))
Là encore je récupère la durée renseignée en utilisant
IntegerArgumentType#getInteger
en indiquant bienduration
qui est le nom du nœud que j’ai indiqué plus tôt.Il semblerait que nous ayons terminé la partie concernant les entités. Passons maintenant à celle concernant les blocs.
Nous allons tout d’abord ajouter une fonction pour mettre en feu les blocs autour d’un certain rayon.
/** * Mets les blocs autour de la source en feu * @param src La source de la commande * @param radius Le rayon d'action * @return le nombre de blocs mis en feu */ private static int setFireBlocks(CommandSource src, int radius) { Vec3d srcPos = src.getPos(); World world = src.getWorld(); int count = 0; for(int x = -radius; x < radius; x++) { for(int z = -radius; z < radius; z++) { BlockPos pos = new BlockPos(srcPos.x + x, srcPos.y, srcPos.z + z); IBlockState state = world.getBlockState(pos); if(state.getBlock() == Blocks.AIR) { world.setBlockState(pos, Blocks.FIRE.getDefaultState()); count++; } } } src.sendFeedback(new TextComponentString(count + " blocks set on fire !"), false); return count; }
Maintenant nous allons ajouter la branche relative aux blocs à notre commande. Rappelez-vous la commande est actuellement dans cet état :
public static void register(CommandDispatcher<CommandSource> dispatcher) { dispatcher.register( LiteralArgumentBuilder.<CommandSource>literal("setfire") .requires(src -> src.hasPermissionLevel(2)) .then( Commands.literal("entities") // [...] ) // Le reste du code va aller ici ); }
Rajoutons un nœud littéral nommé
blocks
puis encore un nœud de type argument qui s’appelleraradius
et qui sera un entier positif :.then( Commands.literal("blocks") .then( Commands.argument("radius", IntegerArgumentType.integer(0)) ) )
Il ne manque plus qu’à appeler notre fonction
setFireBlocks
lors de l’exécution :.then( Commands.literal("blocks") .then( Commands.argument("radius", IntegerArgumentType.integer(0)) .executes(ctx -> setFireBlocks(ctx.getSource(), IntegerArgumentType.getInteger(ctx, "radius"))) ) )
Rien de nouveau par rapport à ce que nous avons vu précédemment, ceci sert juste d’exemple supplémentaire, c’est pourquoi je passe rapidement dessus.
À présent, il faut appeler
SetFireCommand#register
sinon nous n’aurons jamais notre commande en jeu. Pour cela rendez-vous dans la classe principale de votre mod et ajoutez la méthode suivante :private void serverStartingEvent(FMLServerStartingEvent event) { SetFireCommand.register(event.getCommandDispatcher()); }
Dans cette méthode, nous enregistrons notre commande en lui passant une instance de
CommandDispatcher
. Pour le moment, celle-ci n’est pas appelée, pour résoudre ce problème, ajoutez ceci dans le constructeur de votre classe principale :public ModTutorial() { // [...] MinecraftForge.EVENT_BUS.addListener(this::serverStartingEvent); }
Et voilà, le tour est joué. Vous pouvez lancer votre jeu et tester votre commande.
Résultat
Retrouvez le commit relatif à ce tutoriel sur le Github de Minecraft Forge France
Crédits
Ce tutoriel rédigé par @BrokenSwing corrigé par @robin4002 et @DiabolicaTrix et publié sur Minecraft Forge France est mis à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International
-
-
Et pour les commandes clients ?
C’est à dire vous installez le mod uniquement sur le client, vous rejoignez le serveur et vous tapez la commande /test, ça vous donne une information -
@cocoraid
Je sais pas si c’est excatement la même chose en 1.13.2 mais en 1.12.2
On utiliseClientCommandHandler.instance.registerCommand(command);
-
Bonjour à tous,
Le sujet date de 2019 et plusieurs class ont changé entre la 1.13.2 et la 1.15.2 alors petite mise à jour sur les changements (pour ne pas C/C le tuto et faire 3 changements)
TextComponentString
devientStringTextComponent
:src.sendFeedback(new TextComponentString(count + " blocks set on fire !"), false); src.sendFeedback(new TextComponentString(targets.size() + " entities burnt !"), false);
src.sendFeedback(new StringTextComponent(count + " blocks set on fire !"), false); src.sendFeedback(new StringTextComponent(targets.size() + " entities burnt !"), false);
IBlockState
devientBlockState
:IBlockState state = world.getBlockState(pos);
BlockState state = world.getBlockState(pos);
multipleEntities
deviententities
:Commands.argument("targets", EntityArgument.multipleEntities())
Commands.argument("targets", EntityArgument.entities())
Je n’ai pas bien compris ceci:
MinecraftForge.EVENT_BUS.addListener(this::serverStartingEvent);
Je penses que@SubscribeEvent
remplace cela:@SubscribeEvent public void onServerStarting(FMLServerStartingEvent event) { SetFireCommand.register(event.getCommandDispatcher()); }
En espérant que ça puisse aider les débutants !
Bonne programmation
J’ai répondu au mauvais message ^^’
-
Merci pour ces précisions
J’ajoute que je suis sur la 1.16.1 actuellement, et que :
event.getCommandDispatcher()
est devenu :
event.getServer().getCommandManager().getDispatcher()
-
-
-