Les commandes


  • Rédacteurs

    En 1.13 Mojang a décidé de changer entièrement le système de commande 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 ai été développée pour Minecraft, elle peut être utilisée dans bien d'autre 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ésente une donnée qui sera donné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 chaine 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é c'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 tous ici, vous en 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é les entités brûle 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ée 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 alentours sur un certain rayon.

    /**
     * Met 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 to 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 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 MinecraftForgeFrance

    Crédits

    Creative Commons

    Ce tutoriel rédigé par @BrokenSwing corrigé par @robin4002 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


Log in to reply