Les commandes


  • Moddeurs confirmés Rédacteurs

    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 :

    firefox_2019-03-18_23-36-12.png

    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-dessus time <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 que message <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 objet CommandDispatcher<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 un LiteralArgumentBuilder. La chaîne de caractère passée dans ce builder sera le nom de notre commande (dans mon cas je met donc setfire).

    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éthode ArgumentBuilder#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 (ici targets 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 commande

    Commands.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 (ici targets).

    Attention

    Il est important que le nom passé dans EntityArgument#getEntitiesAllowingNone corresponde au nom donné dans Commands#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 appelant IntegerArgumentType#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 bien duration 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’appellera radius 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

    java_2019-03-19_01-09-30.png
    java_2019-03-19_01-23-50.png
    java_2019-03-19_01-23-59.png

    Retrouvez le commit relatif à ce tutoriel sur le Github de Minecraft Forge France

    Crédits

    Creative Commons

    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

    retourRetour vers le sommaire des tutoriels



  • 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 utilise

    ClientCommandHandler.instance.registerCommand(command);
    

Log in to reply