Navigation

    • Register
    • Login
    • Search
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    1. Home
    2. BrokenSwing
    • Profile
    • Following 0
    • Followers 7
    • Topics 37
    • Posts 1156
    • Best 84
    • Groups 2

    BrokenSwing

    @BrokenSwing

    Moddeurs confirmés Rédacteurs

    250
    Reputation
    2348
    Profile views
    1156
    Posts
    7
    Followers
    0
    Following
    Joined Last Online
    Location Drôme

    BrokenSwing Follow
    Moddeurs confirmés Rédacteurs

    Best posts made by BrokenSwing

    • RE: Question pour progresser.

      Salut,

      Effectivement, pour commencer il est plus simple de suivre les différents tutoriels que l’on peut trouver sur Internet (j’ai personnellement commencé le modding en suivant les tutoriels de MFF), après tout, c’est très confortable de se reposer dessus au début.

      Cependant, si tu comptes progresser rapidement dans le milieu du modding je te donnerai plusieurs conseils :

      • Lorsque tu lis les tutoriels, je te conseille de mettre un point d’honneur à comprendre TOUT le code que tu écris, c’est si facile de copier/coller du code sans comprendre ce qu’il fait mais si peu efficace (car ceux qui font ça on besoin de revenir sur le tutoriel à chaque fois qu’ils veulent reproduire la feature).

      • N’hésite pas à lire le code source de Forge, et de Minecraft. Cela te permettra d’avoir une très bonne idée de la manière dont chaque donnée est gérée et quelles méthodes sont appelées quand et où. De plus, certaines fonctionnalités n’ont pas de tutoriel dédié sur le forum (généralement les fonctionnalités avancées), c’est donc dans le code source que tu trouvera le plus d’informations sur leur fonctionnement.

      • Code dans les dernières version. Je veux dire par là que, sans revenir dans l’éternel débat avec la 1.7.10, la plupart des moddeurs compétents développent dans les versions récentes pour plusieurs raisons (je suis conscient qu’il y a aussi des raisons qui poussent certains développeurs à modder dans d’ancienne versions) :

        • Celles-ci ont des fonctionnalités rendant le modding plus agréable
        • Elles ont un meilleur support (cf. le forum de MinecraftForge qui ne fait plus de support pour les vieilles versions)
        • Elles sont plus performantes (des fois, pas tout le temps 😄 )
        • Elles sont mieux mappées (cf. les mappings MCP)
      • Demande de l’aide, mais en étant le plus clair possible. Il faut éviter le problème xy et il faut utiliser un vocabulaire précis (fonction != méthode, classe != interface, etc …). Ce cette manière, les réponses seront elles aussi plus claires (cela t’évitera aussi les “Laisse moi consulter ma boule de cristal”).

      • Pour en revenir aux tutoriels : si, lors de leur lecture, il y a un point que tu ne trouves pas très clair, qui n’est pas très bien expliqué, contact un rédacteur ou un modérateur afin qu’il puisse clarifier le tutoriel. Cela te profitera et profitera aussi aux autres.

      Pour terminer je te conseil, si ce n’est pas déjà le cas, de rejoindre le Discord de MinecraftForgeFrance, on peut y aborder différents sujets assez intéressants que tu ne trouvera pas vraiment sur le forum (chose qui est dommage quand j’y pense. Je vais me pencher sur ce point là, tient.)

      Sur ce, je te souhaite de t’amuser en moddant et surtout de ne pas désespérer si tu as quelques difficultés au début.

      TL;DR : Dommage pour toi

      posted in Discussion générale
      BrokenSwing
      BrokenSwing
    • Créer une configuration pour votre mod

      Introduction

      Il est parfois nécessaire de créer un fichier de configuration pour votre mod afin de permettre à l’utilisateur de personnaliser le comportement de ce dernier ou encore de fournir des données nécessaires au bon fonctionnement de celui-ci. Deux exemples de cas d’utilisation pourraient être :

      • Récupérer les identifiants de connexion à une base de donnée dont votre mod à besoin
      • Permettre à l’utilisateur de régler les dégâts de la super épée que vous venez de créer

      Sommaire du tutoriel

      • Pré-requis
      • Classe des configurations
      • Ajouter des attributs configurables
      • Utiliser un attribut configurable
      • Aller plus loin
      • Résultat
      • Licence et attribution

      Pré-requis

      • Créer la base de son mod
      • Utiliser les événements (nécessaire seulement pour comprendre les exemples)

      Classe des configurations

      Nous allons regrouper toutes nos variables configurables dans une même classe. Pour ce faire, créez une nouvelle classe. De mon côté, je vais l’appeler ConfigTutorial. De plus, je vais créer deux classes internes que je vais nommer Client et Server qui contiendront respectivement les configurations relatives au client et au serveur.

      Voici donc à quoi ressemble ma classe (vide) :

      public class ConfigTutorial
      {
      
          public static class Server
          {
      
          }
      
          public static class Client
          {
      
          }
      
      }
      

      Nous allons ensuite créer un constructeur à nos classes Server et Client qui prend en paramètre une instance de ForgeConfigSpec.Builder :

      public class ConfigTutorial
      {
      
          public static class Server
          {
      
              public Server(ForgeConfigSpec.Builder builder)
              {
      
              }
      
          }
      
          public static class Client
          {
      
              public Client(ForgeConfigSpec.Builder builder)
              {
      
              }
      
          }
      
      }
      

      Le builder va vous permettre de définir les valeurs à récupérer ainsi que d’autres options telles que le commentaire associé à cet attribut, la clé de traduction à utiliser pour afficher cet attribut dans le GUI de config, etc. Il vous permettra aussi de créer des catégories pour classifier vos attributs.

      Avant de définir ces attributs, nous allons déjà mettre tout en place pour qu’ils soient pris en compte. On va donc créer 4 variables statiques dans notre classe. Une de type Server, une de type Client et deux de type ForgeConfigSpec.

      public class ConfigTutorial
      {
      
          public static final ForgeConfigSpec SERVER_SPECS;
          public static final Server SERVER;
      
          public static final ForgeConfigSpec CLIENT_SPECS;
          public static final Client CLIENT;
      
          // [...]
      
      }
      

      Pour le moment, ces variables ne contiennent pas de valeur, on va donc leur en attribuer. Pour cela, nous allons créer un builder, lui passer le constructeur de la classe Client ou Server (on fera l’un puis l’autre) et l’on récupérera une Pair. Passons au code :

      public class ConfigTutorial
      {
      
          public static final ForgeConfigSpec CLIENT_SPECS;
          public static final Client CLIENT;
          
          public static final ForgeConfigSpec SERVER_SPECS;
          public static final Server SERVER;
          
          static 
          {
              Pair<Server, ForgeConfigSpec> serverPair = new ForgeConfigSpec.Builder().configure(Server::new);
              SERVER_SPECS = serverPair.getRight();
              SERVER = serverPair.getLeft();
      
              Pair<Client, ForgeConfigSpec> clientPair = new ForgeConfigSpec.Builder().configure(Client::new);
              CLIENT_SPECS = clientPair.getRight();
              CLIENT = clientPair.getLeft();
          }
      
          // [...]
      
      }
      

      Maintenant que nous avons donné une valeur à nos variables, nous allons pouvoir indiquer à Forge les spécifications de notre configuration pour le client et le serveur. Pour cela, direction la classe principale, dans le constructeur de celle-ci. On y rajoute deux appels à ModLoadingContext#registerConfig, un pour le client et l’autre pour le serveur.

      @Mod(ModTutorial.MOD_ID)
      public class ModTutorial {
      
          // [...]
      
          public ModTutorial() {
              // [...]
              ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ConfigTutorial.SERVER_SPECS);
              ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, ConfigTutorial.CLIENT_SPECS);
          }
          
          // [...]
      
      }
      

      Ajouter des attributs configurables

      Maintenant que tout cela est mis en place, Forge va charger la configuration voulue suivant que vous soyez sur le client ou sur le serveur. Les fichiers de configuration sont au format TOML. Ils sont créés au démarrage du client ou du serveur dans le dossier config.

      Pour ajouter un attribut à ce fichier, il suffit de retourner dans la classe ConfigTutorial. Nous allons, par exemple, ajouter la possibilité au client de désactiver l’accès au GUI de la liste des mods. Pour cela nous allons faire quelques appels au builder passé en paramètre du constructeur de notre classe ConfigTutorial.Client.

      public static class Client
      {
      
          public final ForgeConfigSpec.BooleanValue allowModsGUI;
      
          public Client(ForgeConfigSpec.Builder builder)
          {
              builder.comment("Configuration du client")
                      .push("client");
      
              allowModsGUI = builder
                      .comment("Indique si le GUI de la liste des mods peut s'ouvrir")
                      .define("allowModsGUI", false);
      
              builder.pop();
          }
      
      }
      

      Voyons un peu ce que nous avons fait ici. Dans un premier temps, nous avons créé une nouvelle catégorie nommée client via la méthode push à laquelle nous avons ajouté un commentaire via la méthode comment. Il faut appeler la méthode comment avant ce que nous voulons commenter.

      Nous avons ensuite créer un attribut allowModsGUI de type BooleanValue, cet attribut nous permettra de récupérer la valeur se trouvant dans le fichier de configuration. Nous initialisons cet attribut avec la méthode define. La méthode define possède de nombreuses surcharges suivant le type de retour voulu. Ici, on passe en paramètre le nom de l’attribut que l’on retrouvera dans le fichier de configuration ainsi que sa valeur par défaut.

      Et enfin, nous appelons la méthode pop pour indiquer que nous sortons de la catégorie que nous avons créé précédemment (ici client).

      Nous avons donc créé un attribut dans la classe Client que l’on peut utiliser dans le reste de notre code afin de changer le comportement de notre mod en fonction de la valeur se trouvant dans le fichier de configuration.

      Utiliser un attribut configurable

      Nous allons donc utiliser la valeur indiquée par la configuration afin d’interdire ou non l’accès au GUI de la liste des mods. Pour cela nous allons souscrire à l’event GuiOpenEvent, quand celui-ci sera appelé on vérifiera si le GUI qui s’ouvre est celui de la liste des mods. Dans ce cas, on regardera si l’ouverture est permise, si ce n’est pas le cas on annulera l’événement. C’est parti.

      @Mod.EventBusSubscriber(modid = ModTutorial.MOD_ID)
      public class ConfigTutorial
      {
      
          // [...]
      
          @SubscribeEvent
          public static void cancelModsGUI(GuiOpenEvent event)
          {
              if(GuiModList.class.equals(event.getGui().getClass()) && !CLIENT.allowModsGUI.get())
              {
                  event.setCanceled(true);
              }
          }
      
      }
      

      Comme vous pouvez le voir, pour récupérer la valeur se trouvant dans la configuration j’ai simplement appelé BooleanValue#get. Ici, j’ai tout mis dans la même classe mais vous pouvez bien sûr récupérer cette valeur depuis n’importe quel endroit dans votre code.

      Et voilà, à présent vous pouvez lancer votre client, vous devriez trouver un fichier nommé modtutorial-client.toml dans le dossier config de Minecraft avec pour contenu :

      #Configuration du client
      [client]
          #Indique si le GUI de la liste des mods peut s'ouvrir
          allowModsGUI = false
      

      Vous pouvez modifier cette valeur directement depuis le fichier de configuration et tester la différence en essayant de consulter la liste des mods.

      Pour les configurations relatives au serveur, vous pouvez faire exactement pareil dans la classe Server que nous avons créé plus tôt.

      Aller plus loin

      Je vous invite à regarder les méthodes que propose le builder. Il y en a pour les entiers, les booleans, les enums, etc . De plus, vous pouvez définir des clés de traduction pour le texte qui sera affiché dans le GUI de configuration.
      Vous pouvez aussi indiquer que le changement de valeur requiert un redémarrage du monde en cours pour être pris en compte.

      Pour ajouter la possibilité de modifier votre configuration depuis le GUI de configuration accessible depuis la liste des mods, il faut enregistrer une extension via ModLoadingContext#registerExtensionPoint. Il faut enregistrer l’extension ExtensionPoint#CONFIGGUIFACTORY. Cependant, cet appel n’a pas vraiment d’effet, car Forge n’a pas encore ajouté la possibilité de retourner true à ModInfo#hasConfigUI ce qui fait que le bouton pour accéder au GUI de config est désactivé.

      Résultat

      Les différentes modifications du code sont retrouvables sur le commit relatif à ce tutoriel.

      Licence et attribution

      Creative Commons

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

      retour Sommaire des tutoriels

      posted in Autres
      BrokenSwing
      BrokenSwing
    • Les capabilities

      Le système de capability ajouté par Forge est relativement simple et terriblement efficace. Il permet en effet d’exposer des interfaces aux autres mods. Par exemple Forge a créé des capabilities pour différentes choses, il met à disposition des capabilities pour jouer avec les liquides, les items ou encore l’énergie (entre autres). Mais vous pouvez vous aussi exposer votre propre capability ou l’implémentation d’une capability déjà existante.

      C’est donc ce que nous allons voir dans ce tutoriel. Vous apprendrez à utiliser les capabilities déjà exposées par Forge, à créer votre propre capability et à l’exposer.

      Sommaire du tutoriel

      • Pré-requis
      • Comprendre les capabilities
      • Créer une capability
      • Attacher une capability à une classe
      • Utiliser une capability
      • Rendre votre capability persistante après la mort du joueur
      • Synchroniser votre capability
      • Résultat
      • Licence et attribution

      Pré-requis

      • Utiliser les événements
      • Communiquer entre le client et le serveur : le réseau et les paquets (nécessaire seulement si vous voulez synchroniser vos données entre le serveur et le client)

      Comprendre les capabilities

      Avant de commencer à créer ou utiliser des capabilities il est important de comprendre comment celles-ci fonctionnent. Une capability est un objet permettant d’accéder à l’instance d’une interface pour un objet donné. Par exemple la capability CapabilityItemHandler.ITEM_HANDLER_CAPABILITY permet d’accéder à une instance de IItemHandler, tandis que la capability CapabilityEnergy.ENERGY permet d’accéder à une instance de IEnergyStorage.
      Comme vous vous en doutez sûrement, vous ne pouvez cependant pas récupérer ces instances depuis n’importe quel objet, il faut en effet que l’objet soit un ICapabilityProvider (fournisseur de capability, ça a du sens). Il existe plusieurs classes dans Minecraft qui implémentent cette interface, comme Entity, Chunk ou encore ItemStack par exemple. On peut cependant implémenter cette interface nous même dans nos propres classes.
      Donc pour résumer, les capabilities permettant d’exposer des interfaces. Muni d’une capability et d’une instance de ICapabilityProvider on peut récupérer une instance de l’interface associée à la capability.

      Créer une capability

      Voyons comment créer notre propre capability. Si vous avez correctement lu ce qui a été dit plus haut, une capability permet d’exposer une interface, nous allons donc faire notre interface.

      Personnellement je veux implémenter un système d’épuisement dans le jeu, voici donc l’interface que je crée.

      /**
       * Interface permettant de gérer la fatigue. La fatigue est représentée par un entier allant de 0 à 10 000, avec 10 000
       * correspondant au maximum de fatigue.
       */
      public interface IExhaustable
      {
      
          /**
           * Renvoie un nombre entre 0 et 10 000
           * @return la fatigue
           */
          int getExhaustion();
      
          /**
           * Permet de définir la fatigue, celle-ci est un nombre entre 0 et 10 000 (bornes incluses).
           * Dans le cas d'une valeur passée supérieure à 10 000, celle-ci est est plafonnée automatiquement.
           * Dans le cas d'une valeur négative passée, celle-ci est compté comme nulle.
           * @param value La nouvelle valeur de la fatigue
           */
          void setExhaustion(int value);
      
          /**
           * Réduit la fatigue.
           * @param value La quantité de fatigue à enlever
           */
          default void reduceExhaustion(int value) {
              this.setExhaustion(this.getExhaustion() - value);
          }
      
          /**
           * Augmente la fatigue.
           * @param value La quantité de fatigue à ajouter
           */
          default void increaseExhaustion(int value) {
              this.setExhaustion(this.getExhaustion() + value);
          }
      }
      

      C’est une interface tout ce qu’il y a de plus simple.

      La seconde étape est de créer une implémentation par défaut de notre capability. Encore une fois cela va être très simple, à l’image de notre interface :

      public class ExhaustionHolder implements IExhaustable
      {
          private int exhaustion = 0;
      
          @Override
          public int getExhaustion()
          {
              return this.exhaustion;
          }
      
          @Override
          public void setExhaustion(int value)
          {
              this.exhaustion = clamp(value);
          }
      
          private int clamp(int value) {
              if(value > 10000) return 10000;
              if(value < 0) return 0;
              return value;
          }
      }
      

      Et la dernière chose dont nous avons besoin est une implémentation de Capability.IStorage, celle-ci permet d’enregistrer les données relatives à notre interface dans des tags NBT afin de la sauvegarder (qu’on ne perde pas nos données quand quitte le monde). Nous allons faire ceci tout de suite et encore une fois c’est pas une implémentation compliquée.

      public static class DefaultExhaustionStorage implements Capability.IStorage<IExhaustable> {
      
          @Nullable
          @Override
          public INBTBase writeNBT(Capability<IExhaustable> capability, IExhaustable instance, EnumFacing side)
          {
              return new NBTTagInt(instance.getExhaustion());
          }
      
          @Override
          public void readNBT(Capability<IExhaustable> capability, IExhaustable instance, EnumFacing side, INBTBase nbt)
          {
              instance.setExhaustion(((NBTTagInt)nbt).getInt());
          }
      }
      

      Maintenant je vais créer une classe que je vais appeler CapabilityExhaustion dans laquelle je vais rajouter une fonction permettant d’enregistrer ma capability :

      public static void register()
      {
          CapabilityManager.INSTANCE.register(IExhaustable.class, new DefaultExhaustionStorage(), ExhaustionHolder::new);
      }
      

      Pour enregistrer une capability on doit donc appeler CapabilityManager#register en passant l’interface que l’on veut exposer, notre implémentation de Capability.IStorage ainsi que l’implémentation par défaut de notre interface.

      Dans cette même classe je vais ajouter une variable statique qui contiendra l’instance de notre capability (celle permettant de récupérer l’instance de notre interface via une instance de ICapabilityProvider, relisez la permière partie si vous êtes perdus).

      @CapabilityInject(IExhaustable.class)
      public static final Capability<IExhaustable> EXHAUSTION_CAPABILITY = null;
      

      On crée donc une variable de type Capability que l’on annote avec CapabilityInject, cette annotation permettra à Forge de changer la valeur de la variable une fois notre capability enregistrée via le CapabilityManager.

      On va maintenant appeler notre fonction register sinon notre capability ne sera jamais enregistrée. Pour cela je me rend dans la classe principale de mon mod et j’appelle ma fonction dans l’event FMLCommonSetupEvent :

      @Mod(ModTutorial.MOD_ID)
      public class ModTutorial {
      
      	// [...]
      
      	public ModTutorial() {
      		FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
      		// [...]
      	}
      
      	private void setup(final FMLCommonSetupEvent event) {
      		// [...]
      		CapabilityExhaustion.register();
      	}
      
              // [...]
      }
      

      Voilà, nous avons créer notre capability et nous l’avons enregistrée. Cependant nous ne l’utilisons pas, et elle ne sert pas à grand chose. Rendez-vous dans la suite du tutoriel.

      Attacher une capability à une classe

      Afin qu’on puisse récupérer une capability depuis l’instance d’une classe, il faut que celle-ci implémente l’interface ICapabilityProvider. Cette interface possède une méthode à implémenter (et une autre dont l’implémentation par défaut est suffisante donc nous nous en occuperont pas). La méthode de cette interface permet donc de récupérer une capability, cependant il faut savoir que ce n’est pas parce qu’une classe implémente ICapabilityProvider qu’elle possède forcément votre capability (par exemple on imagine mal un ItemStack être fatigué, il ne possèdera donc sûrement pas notre capability). Afin d’être sûr que vous soyez parfaitement conscient de ce que vous faites quand vous manipulez les capabilities, Forge demande de retourner un LazyOptional qui doit être empty() si l’instance ne possède pas la capability, ou alors of(instance) où instance est l’instance de l’interface associée à la capability demandée.

      Dans le cas d’une classe qui nous appartient il est très simple d’implémenter cette interface mais imaginons que je veuille ajoute ma capability aux entités, je n’ai bien-heureusement pas la possibilité de modifier le code source de cette classe. Comme vous pouvez vous en douter Forge a ajouté un event pour pouvoir faire ce genre de choses. Cet event est AttachCapabilitiesEvent, et nous ajoutons notre capability via AttachCapabilitiesEvent#addCapability(ResourceLocation, ICapabilityProvider). Comme vous pouvez le voir il nous faut là aussi une instance de ICapabilityProvider donc nous allons implémenter une classe comme si vous voulions lui donner notre capability puis nous en passerons une instance à cette méthode. La ResourceLocation est une clé permettant d’identifier de manière unique votre capability (elle sert pour la sauvegarde).

      Nous allons donc créer une implémentation de ICapabilityProvider pour notre capability, et plus précisement nous allons implémenter ICapabilitySerializable afin que Forge sache qu’il faut sauvegarder notre capability.
      Voici donc notre implémentation :

      public class PlayerExhaustionWrapper implements ICapabilitySerializable<INBTBase>
      {
          private IExhaustable holder = CapabilityExhaustion.EXHAUSTION_CAPABILITY.getDefaultInstance();
          private final LazyOptional<IExhaustable> lazyOptional = LazyOptional.of(() -> this.holder);
      
          @Nonnull
          @Override
          public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable EnumFacing side)
          {
              return CapabilityExhaustion.EXHAUSTION_CAPABILITY.orEmpty(cap, lazyOptional);
          }
      
          @Override
          public INBTBase serializeNBT()
          {
              return CapabilityExhaustion.EXHAUSTION_CAPABILITY.writeNBT(this.holder, null);
          }
      
          @Override
          public void deserializeNBT(INBTBase nbt)
          {
              CapabilityExhaustion.EXHAUSTION_CAPABILITY.readNBT(holder, null, nbt);
          }
      }
      

      Il ne devrait rien à avoir de surprenant, je crée une instance de mon interface (celle par défaut) puis lorsque quelqu’un appelle getCapability en passant en paramètre ma capability (CapabilityExhaustion.EXHAUSTION_CAPABILITY) je retourne cette instance. Ce mécanisque de vérification est déjà fait pour nous dans Capability#orEmpty car c’est un comportement que la majorité des moddeurs et Forge utilisent.

      Pour ce qui est de serializeNBT et deserializeNBT, je redirige simplement les appels vers l’implémentation de Capability.IStorage que j’ai founi lors de l’enregistrement de ma capability.

      On peut donc maintenant fournir une instance de cette implémentation à l’event AttachCapabilitiesEvent. Donc ma classe CapabilityExhaustion que j’annote préalablement de @Mod.EventBusSubscriber(modid = ModTutorial.MOD_ID), j’ajoute la variable suivante :

      public static final ResourceLocation CAP_KEY = new ResourceLocation(ModTutorial.MOD_ID, "exhaustion");
      

      Ainsi que l’event pour attacher ma capability aux entités (je veux que les entités subissent la fatigue) :

      @SubscribeEvent
      public static void attachToEntities(AttachCapabilitiesEvent<Entity> event)
      {
          if(event.getObject() instanceof EntityLivingBase && !event.getObject().world.isRemote)
          {
              PlayerExhaustionWrapper wrapper = new PlayerExhaustionWrapper();
              event.addCapability(CAP_KEY, wrapper);
          }
      }
      

      Ici ma condition permet de vérifier que l’entité est une entité vivante et aussi que je sois côté serveur.

      Important

      Ne faites pas d’opération trop lourde pendant cet event car il est appelé à l’instantiation de chaque entité (dans notre cas), ce qui correspond à un nombre d’appels assez conséquent.

      Note

      Si nous avions voulu attacher notre capability à un ItemStack nous souscrit à l’événement AttachCapabilitiesEvent<ItemStack>. Les type génériques valides sont Entity, ItemStack, Chunk, TileEntity et World .

      Voilà, maintenant toutes les instance de EntityLivingBase côté serveur possèdent notre capability.

      Utiliser une capability

      Nous allons donc maintenant apprendre à utiliser une capability et plus précisément notre capability mais gardez bien en tête que les méthodes à appeler pour utiliser d’autre capabilities sont exactement les mêmes que celles que nous allons voir. En réalité il n’y a qu’une seule méthode à utiliser : ICapabilityProvider#getCapability.
      Je vais mettre ici beaucoup de code dans lequel je récupére une instance de LazyOptional après avoir appelé ICapabilityProvider#getCapability puis je l’utilise pour effectuer un traitement. J’utilise la méthode LazyOptional#ifPresent. Si vous n’êtes pas à l’aise avec les Optional je vous invite à consulter la documentation de Java.

      @Mod.EventBusSubscriber(modid = ModTutorial.MOD_ID)
      public class ExhaustionModificationsHandler
      {
          private static final UUID MODIFIER_ID = UUID.fromString("73c800ed-7da9-4a6f-8fe1-b106096269f6");
      
          @SubscribeEvent
          public static void onLivingJump(LivingEvent.LivingJumpEvent event)
          {
              event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY)
                      .ifPresent(cap -> cap.increaseExhaustion(50));
          }
      
          @SubscribeEvent
          public static void onLivingTick(LivingEvent.LivingUpdateEvent event)
          {
              event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY)
                      .ifPresent(cap -> {
                          cap.reduceExhaustion(2);
                          if(event.getEntity().motionX == 0 && event.getEntity().motionY == 0 && event.getEntity().motionZ ==0)
                          {
                              cap.reduceExhaustion(4);
                          }
                          if(event.getEntity().isSprinting())
                          {
                              cap.increaseExhaustion(3);
                          }
                          if(event.getEntity().isSwimming())
                          {
                              cap.increaseExhaustion(3);
                          }
      
                          // Operations :
                          //  0 : valeur à ajouter à la valeur de base
                          //          valeurFinale = valeurBase + quantite
                          //  1 : pourcentage de la valeur de base à ajouter
                          //          valeurFinale += valeurBase * quantite
                          //  2 : pourcentage de la valeur finale à rajouter en plus
                          //          valeurFinale *= 1 + quantite
                          AttributeModifier modifier = new AttributeModifier(
                                  MODIFIER_ID,
                                  "exhaustion",
                                  - cap.getExhaustion() / 10_000f,
                                  2
                          );
      
                          updateModifierFor(EntityLivingBase.SWIM_SPEED, modifier, event.getEntityLiving());
                          updateModifierFor(SharedMonsterAttributes.MOVEMENT_SPEED, modifier, event.getEntityLiving());
                          updateModifierFor(SharedMonsterAttributes.ATTACK_SPEED, modifier, event.getEntityLiving());
                      });
          }
      
          @SubscribeEvent
          public static void onLivingEat(LivingEntityUseItemEvent event)
          {
              event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(capa -> {
                  if(event.getItem().getItem() instanceof ItemFood)
                  {
                      ItemFood food = (ItemFood) event.getItem().getItem();
                      capa.reduceExhaustion(food.getUseDuration(event.getItem()));
                  }
              });
          }
      
          @SubscribeEvent
          public static void onLivingAttack(LivingAttackEvent event)
          {
              event.getEntity().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(capa -> capa.increaseExhaustion(50));
          }
      
          private static void updateModifierFor(IAttribute attribute, AttributeModifier modifier, EntityLivingBase entity)
          {
              IAttributeInstance instance = entity.getAttribute(attribute);
              if(instance.hasModifier(modifier))
              {
                  instance.removeModifier(modifier);
              }
      
              if(entity instanceof EntityPlayerMP) {
                  EntityPlayerMP player = (EntityPlayerMP) entity;
                  if(player.isCreative() || player.isSpectator()) return;
              }
      
              instance.applyModifier(modifier);
          }
      }
      

      À chaque fois je récupère le LazyOptional et si il contient une instance de mon interface j’effectue un traitement avec. Pour résumer le code, quand le joueur effectue certaines actions (courir, sauter, nager, prendre des dégâts) il accumule de la fatigue, quand il mange il perd de la fatigue et il perd aussi de la fatigue au cours du temps. Plus le joueur est fatigué, plus il est lent.

      Rendre votre capability persistante après la mort du joueur

      Si vous avez déjà un peu testé le code que l’on a fait jusqu’à maintenant vous vous êtes peut-être rendu compte que si vous mourrez, vous ne conservez pas la fatigue que vous avez accumulée avant votre mort. Dans le cas de la fatigue ce n’est pas très grave mais il peut arriver que vous vouliez garder le même état.

      Pour cela nous allons un peu modifier le contenu de notre classe CapabilityExhaustion, je vais ajouter une Map qui me permettra de stocker l’instance de mon interface (celle associée à ma capability) telle qu’elle était avant le respawn du joueur afin de pouvoir la restorer par la suite :

      private static final Map<Entity, IExhaustable> INVALIDATED_CAPS = new WeakHashMap<>();
      

      Attention

      Il est important d’avoir une WeakHashMap ici afin de ne pas créer de fuite de mémoire.

      Ensuite dans l’event pour attacher ma capability aux entités, je stock l’instance de mon interface dans le cas où l’entité est un joueur (seuls le joueurs meurent est respawn) :

      @SubscribeEvent
      public static void attachToEntities(AttachCapabilitiesEvent<Entity> event)
      {
          if(event.getObject() instanceof EntityLivingBase && !event.getObject().world.isRemote)
          {
              PlayerExhaustionWrapper wrapper = new PlayerExhaustionWrapper();
              event.addCapability(CAP_KEY, wrapper);
              if(event.getObject() instanceof EntityPlayer)
              {
                  event.addListener(() -> wrapper.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(cap -> INVALIDATED_CAPS.put(event.getObject(), cap)));
              }
          }
      }
      

      Puis je souscrit à l’event PlayerEvent.Clone qui est appelé au respawn du joueur (après sa mort ou après avoir passé un portail). Pour savoir si le joueur vient de mourir on a la méthode PlayerEvent.Clone#isWasDeath. Si le joueur était mort on récupère donc les données de l’ancienne instance de notre interface et on les passe à la nouvelle instance de notre capability :

      @SubscribeEvent
      public static void copyCapabilities(PlayerEvent.Clone event)
      {
          if(event.isWasDeath())
          {
              event.getEntityPlayer().getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(newCapa -> {
                  if(INVALIDATED_CAPS.containsKey(event.getOriginal()))
                  {
                      INBTBase nbt = CapabilityExhaustion.EXHAUSTION_CAPABILITY.writeNBT(INVALIDATED_CAPS.get(event.getOriginal()), null);
                      CapabilityExhaustion.EXHAUSTION_CAPABILITY.readNBT(newCapa, null, nbt);
                  }
              });
          }
      }
      

      Synchroniser votre capability

      Lorsque que vous créez une capability, les données relatives à celle-ci ne sont pas synchronisées automatiquement entre le client et le serveur. Dans notre cas, la capability que j’ai créé n’est :

      • Premièrement : pas attachée à aux entités côté client car j’ai mis la condition !event.getObject().world.isRemote qui permet de s’assurer que l’on se trouve côté serveur.

      • Deuxièmement : pas synchronisée avec le client. En effet celui-ci n’a aucun moyen de connaitre son état de fatigue actuel, cela pose un problème si nous voulons par exemple afficher celui-ci sur un GUI.

      Nous allons donc voir comment synchroniser notre capability avec le client concerné afin de pouvoir y accéder côté client. Je ne vais vous montrer ici que le paquet que je vais utiliser car vous avez bien sûr les pré-requis.

      Pour commencer je vais donc créer un simple paquet qui aura pour rôle de transporter la valeur de la fatigue du serveur vers le client. À sa réception je change la valeur de la fatigue sur le client :

      public class SyncExhaustionPacket
      {
          private int exhaustion;
      
          public SyncExhaustionPacket(IExhaustable instance) {
              this.exhaustion = instance.getExhaustion();
          }
      
          public SyncExhaustionPacket(int exhaustion)
          {
              this.exhaustion = exhaustion;
          }
      
          public static void encode(SyncExhaustionPacket pck, PacketBuffer buf)
          {
              buf.writeInt(pck.exhaustion);
          }
      
          public static SyncExhaustionPacket decode(PacketBuffer buf)
          {
              return new SyncExhaustionPacket(buf.readInt());
          }
      
          public static void handle(SyncExhaustionPacket pck, Supplier<NetworkEvent.Context> ctxSupplier)
          {
              if(ctxSupplier.get().getDirection().getReceptionSide() == LogicalSide.CLIENT)
                  ctxSupplier.get().enqueueWork(() -> handleClientUpdate(pck));
              ctxSupplier.get().setPacketHandled(true);
          }
      
          @OnlyIn(Dist.CLIENT)
          private static void handleClientUpdate(SyncExhaustionPacket pck)
          {
              Minecraft.getInstance().player.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY)
                      .ifPresent(capa -> capa.setExhaustion(pck.exhaustion));
          }
      
      }
      

      On veut pouvoir envoyer ce paquet dès que la valeur de la fatigue change. C’est pourquoi je vais créer une implémentation dédiée pour les joueurs côté serveur afin qu’elles prennent en paramètre un joueur :

      public class PlayerExhaustionHolder extends ExhaustionHolder
      {
      
          private EntityPlayerMP player;
      
          public PlayerExhaustionHolder(EntityPlayerMP player)
          {
              this.player = player;
          }
      
          @Override
          public void setExhaustion(int value)
          {
              super.setExhaustion(value);
              // setExhaustion peut être appelé trop tôt et que player.connection soit null
              if (player.connection != null)
              {
                  player.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY)
                          .ifPresent(capa -> TutorialNetwork.CHANNEL.send(
                                  PacketDistributor.PLAYER.with(() -> this.player),
                                  new SyncExhaustionPacket(capa))
                          );
              }
          }
      }
      

      A chaque fois que la valeur change, on envoie un paquet au client concerné. On va aussi modifier un peu l’implémentation dans notre classe PlayerExhaustionWrapper afin de pouvoir renseigner l’instance de IExhaustable voulue :

      public class PlayerExhaustionWrapper implements ICapabilitySerializable<INBTBase>
      {
      
          private IExhaustable holder;
          // [...]
      
          public PlayerExhaustionWrapper(IExhaustable holder) {
              this.holder = holder;
          }
      
          // [...]
      
      }
      

      Et puis on modifie ce qui se trouve dans l’événement pour attacher notre capability aux entités. Rendez-vous donc dans le classe CapabilityExhaustion à la fonction attachToEntities(AttachCapabilitiesEvent<Entity>). Suivant si l’entité est une instance de EntityPlayerMP ou non, on va instancier l’une ou l’autre de nos implémentations de IExhausable. Je vais aussi retirer la condition vérifiant que nous sommes côté serveur, cependant dans la classe ExhaustionModificationsHandler je vérifierai bien à chaque fois que je sois sur le serveur afin de modifier ma valeur seulement côté serveur puis de la synchroniser :

      @SubscribeEvent
      public static void attachToEntities(AttachCapabilitiesEvent<Entity> event)
      {
          if(event.getObject() instanceof EntityLivingBase)
          {
              IExhaustable holder;
              if(event.getObject() instanceof EntityPlayerMP)
              {
                  holder = new PlayerExhaustionHolder((EntityPlayerMP)event.getObject());
              }
              else
              {
                  holder = CapabilityExhaustion.EXHAUSTION_CAPABILITY.getDefaultInstance();
              }
      
              PlayerExhaustionWrapper wrapper = new PlayerExhaustionWrapper(holder);
              event.addCapability(CAP_KEY, wrapper);
              
              if(event.getObject() instanceof EntityPlayer)
              {
                  event.addListener(() -> wrapper.getCapability(CapabilityExhaustion.EXHAUSTION_CAPABILITY).ifPresent(cap -> INVALIDATED_CAPS.put(event.getObject(), cap)));
              }
          }
      }
      

      Et voilà, tout devrait fonctionner. Pour vérifier cela je met un point d’arrêt au niveau de l’arrivé du paquet sur le client et je vois que je reçois bien la valeur de la fatigue :

      2019-06-10_14-28-26.png

      La mise à jour se bien fait sur le client.

      Résultat

      Les différentes modifications du code sont retrouvables sur le Github de MinecraftForgeFrance.

      Vous pouvez vous référer aux différents commits :

      • Créer une capability
      • Attacher une capability à une classe
      • Utiliser une capability
      • Rendre votre capability persistante après la mort du joueur
      • Synchroniser votre capability

      Licence et attribution

      Creative Commons

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

      retour Sommaire des tutoriels

      posted in Autres
      BrokenSwing
      BrokenSwing
    • Les fichiers de texture et modèles JSON

      Sommaire

      • Introduction
      • Pré-requis
      • Code
        • Les états de bloc
        • Les modèles de bloc
        • Les modèles des objets
      • Bonus
        • Créer une barrière
        • Créer un modèle pour les tuyaux/câbles de vos mods
      • Résultat

      Introduction

      Ce tutoriel a pour but d’expliquer le fonctionnement des fichiers JSON utilisés pour les modèles des blocs et des objets.

      Pré-requis

      • Créer un bloc basique
      • Créer un item simple

      Code

      Les états de bloc

      Pour les blocs, 2 fichiers JSON sont nécessaires, le premier se trouve dans la package assets.modid.blockstates où bien entendu
      il faut remplacer modid par l’ID de votre mod.
      Ce fichier JSON aura pour fonction d’indiquer quel modèle utiliser pour rendre le bloc, selon son état. On peut le voir comme un aiguillage.
      Nous allons donc créer ce fichier, il devra avoir pour nom, le nom sous lequel il est enregistré.
      Pour ce bloc ci :

      public static final Block blockSimpleTexture = new Block(Material.ROCK).setUnlocalizedName("blockSimpleTexture").setRegistryName("blockSimpleTexture");
      

      Le nom du fichier JSON sera celui mis en paramètre de setRegistryName(String name), donc “blockSimpleTexture”.
      Nous créons donc le fichier, si on prend comme exemple le bloc déclaré ci-dessus on obtient blockSimpleTexture.json se trouvant dans le package assets.modid.blockstates.
      Pour commencer le fichier JSON doit ressembler à cela (structure de base d’un fichier JSON) :

      {
      
      }
      

      Nous allons ajouter un tag variants qui contiendra toute les variantes de notre bloc

      {
          "variants": {
      
          }
      }
      

      Viennent ensuite le nom des variantes de votre bloc, si votre bloc n’a qu’une seule variante, alors son nom est normal, ce qui donne :

      {
          "variants": {
              "normal": {
      
              }
          }
      }
      

      Dans la variantes nous pouvons ensuite indiquer quel model utiliser pour rendre notre bloc, ce model est un fichier JSON qui se trouvera dans le
      package assets.modid.models.block et qui aura pour nom celui que vous allez indiquer.

      {
          "variants": {
              "normal": {
                  "model": "modid:nomDuModel"
              }
          }
      }
      

      Il faut bien sûr remplacer modid et nomDuModel. Ici nous avons indiquer le modèle à utiliser, des tags supplémentaires peuvent venir s’ajouter.
      Grâce au tag x nous pouvons effectuer une rotation du modèle sur l’axe x et grâce au tag y nous pouvons effectuer une rotation sur l’axe y.
      Attention : le tag z, n’existe pas.
      Ces tags de rotation doivent s’incrémenter de 90 en 90. Exemple : 0, 90, 180, 270. Si vous utilisez les tags x ou y vous pouvez aussi utiliser
      le tag uvlock qui permet que la texture ne tourne pas avec le modèle, il peut prendre les valeurs true ou false.
      Voici une exemple non concret avec tout les tags :

      {
          "variants": {
              "normal": {
                  "model": "modid:nomDuModel",
                  "x": 90,
                  "y": 270,
                  "uvlock": true
              }
          }
      }
      

      Nous pouvons aussi associer plusieurs modèles à une seule variantes de la façon suivante :

      {
          "variants": {
              "normal": [{
                  "model": "modid:nomDuModelUn",
                  "x": 90,
                  "weight": 2
              }, {
                  "model": "modid:nomDuModelDeux",
                  "y": 180,
                  "uvlock": true,
                  "weight": 1
              }]
          }
      }
      

      Vous pouvez voir ici 2 choses, premièrement chaque modèles peut avoir des tags x, y et uvlock différents et deuxièmement que l’on peut ajouter un
      tag weight (facultatif) si nous avons plusieurs modèles afin de déterminer, la probabilité d’être choisi, de chaque modèle. Si vous ne mettez pas ce tag, tous les modèles
      auront la même chance d’être utilisé (tous auront pour valeur 1).

      Il existe une autre façon d’indiquer quels modèles utiliser en utilisant le tag multipart à la place du tag variants, sont utilisation permet
      d’afficher plusieurs modèles en même temps. Un bon exemple de son utilisation est la barrière, grâce au multipart, Minecraft affiche le pilier et si d’autres
      blocs sont situés à côté de la barrière alors il affiche les jointures.
      Premièrement il faut mettre le tag multipart :

      {
          "multipart": [
      
          ]
      }
      

      Il contient un tableau de cas, un cas est défini par une condition (facultative) et un/des modèle(s) affiché(s) si la condition est vraie (si il n’y a pas de condition
      le jeu considère qu’elle vaut vraie). Un cas est défini entre accolades et possède donc le tag when qui est facultatif et le tag apply qui est obligatoire.
      On peut traduire cela par : Quand conditions appliquer modèle(s)
      Ajoutons un cas à notre fichier :

      {
          "multipart": [{
              "when": {
      
              },
              "apply": {
      
              }
          }]
      }
      

      Dans le tag when on peut trouver soit le tag OR, soit des conditions. Commençons par les conditions :
      Imaginons que j’ai une propriété nommée ready est une autre nommée dead, je veux appliquer le modèle modid:modelReady si ready vaut “true”.

      {
          "multipart": [{
              "when": {
                  "ready": "true"
              },
              "apply": {
                  "model": "modid:modelReady"
              }
          }]
      }
      

      Ici nous n’avons qu’une condition mais nous pouvons en ajouter d’autres, si je veux par exemple que le modèle soit afficher seulement si dead vaut “false” et que ready vaut “true” alors:

      {
          "multipart": [{
              "when": {
                  "ready": "true",
                  "dead": "false"
              },
              "apply": {
                  "model": "modid:modelReady"
              }
          }]
      }
      

      Vous pouvez voir qu’on peut ajouter pleins de conditions à la suite, il faudra que toutes les conditions soient vraies pour que le modèle soit appliqué.
      Je tient à préciser que l’on peut spécifier plusieurs modèles dans le tag apply de la même façon qu’énoncé au début :

      {
          "multipart": [{
              "when": {
                  "ready": "true",
                  "dead": "false"
              },
              "apply": [{
                  "model": "modid:modelReadyUn",
                  "x": 90,
                  "weight": 2
              }, {
                  "model": "modid:modelReadyDeux",
                  "y": 180,
                  "weight": 1
              }]
          }]
      }
      

      Pour ce qui est du OR il faut l’utiliser de cette façon :

      {
          "multipart": [{
              "when": {
                  "OR": [{
                      "ready": "false"
                  }, {
                      "dead": "true"
                  }]
              },
              "apply": {
                  "model": "modid:modelNotReadyOrDead"
              }
          }]
      }
      

      Le tag OR est une liste de cas dans lequels il faut appliquer le ou les modèle(s). Un cas se met un accolades et les conditions qui le compose sont
      sous la forme “propriété” : “valeur”, on peut aussi séparer les valeurs avec le symbole | ainsi :

      "orientation": "top|bottom"
      

      Signifie “Si orientation vaut top ou bottom” ou en code if(orientation == “top” || orientation == “bottom”).
      Un dernier pour bien comprendre la logique :

      {
          "multipart": [{
              "when": {
                  "OR": [{
                      "ready": "false",
                      "orientation": "south|north"
                  }, {
                      "dead": "true"
                  }]
              },
              "apply": {
                  "model": "modid:modelNotReadyOrDead"
              }
          }]
      }
      

      Est équivalent en terme de code à :

      if( (ready == "false" && (orientation == "south" || orientation == "north") ) || dead == "true") {
          apply();
      }
      

      Voilà pour le fichier JSON situé dans le package assets.modid.blockstates

      Les modèles de bloc

      Ce fichier JSON se trouve dans le package assets.modid.models.block, son nom doit correspondre avec celui donné dans le fichier d’état de bloc.
      C’est dans ce fichier que nous allons vraiment définir ce que nous devons afficher. Nous allons commencer avec le tag ambientocclusion, il peut prendre
      comme valeur true ou false, il sert à définir si l’occlusion ambiente doit être utilisé.

      {
          "ambientocclusion": true
      }
      

      Nous allons à présent nous intéresser au tag textures, c’est ici que nous allons déclarer nos textures. Une texture est déclarée par son nom associé au
      chemin de l’image. Si je veux définir une texture qui s’appelerai jambon alors j’aurai le code suivant :

      {
          "ambientocclusion": true,
          "textures": {
              "jambon": "modid:blocks/jambon"
          }
      }
      

      Le chemin de l’image commence à partir du package assets.modid.textures, vous pouvez mettre ce que vous voulez pour le nom de la texture, cependant
      un nom est réservé, c’est particle qui défini la texture pour les particules, donc pour dire que vous voulez des particules de jambon :

      {
          "textures": {
              "particle": "modid:blocks/jambon"
          }
      }
      

      Si vous ne mettez pas votre modid dans le chemin de la texture, l’image sera cherchée dans les fichiers de Minecraft, il est donc tout à fait possible
      d’utiliser une texture de Minecraft.

      Maintenant voyons le tag elements, c’est celui qui définie les cubes à afficher. Ce tag est un tableau :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [
      
          ]
      }
      

      C’est dans ce tableau que les différents cubes vont être définis. Un cube possède deux coordonnées, le début et la fin du cube. Une coordonnée est un tableau
      de 3 nombres pouvant aller de -16 à 32, il a pour forme [x, y, z], il faut savoir qu’un cube de minecraft basique commence en [0, 0, 0] et fini en [16, 16, 16].
      Vous pouvez ainsi rendre un cube sur un volume de 3x3x3 blocs.
      Nous allons définir nos coordonnées pour obtenir un bloc plus petit :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10]
          }]
      }
      

      Comme vous pouvez voir on utilise les tags from et to pour définir les coordonnées. Ici pour obtenons un cube 9 fois plus petit qu’un cube
      normal, et il est centré.
      Nous devons ensuite indiquer quelle texture utiliser pour chaque face du cube. Pour cela nous allons utiliser le tag faces :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "faces": {
                  "down": {
                      "texture": "#stone"
                  },
                  "up": {
                      "texture": "#stone"
                  },
                  "north": {
                      "texture": "#stone"
                  },
                  "south": {
                      "texture": "#stone"
                  },
                  "west": {
                      "texture": "#stone"
                  },
                  "east": {
                      "texture": "#stone"
                  }
              }
          }]
      }
      

      Ceci est le code minimal pour un cube, on utilise le tag texture pour indiquer quelle texture utiliser, on s’aperçoit qu’il faut mettre un # devant
      le nom de la texture que nous avons défini dans le tag textures.
      Si vous supprimez un tag de face, par exemple down, alors cette face ne sera plus rendue, à faire si la face ne devra en aucun cas être rendue.
      Il y a plusieurs autres tags facultatifs pour le tag faces, le tag uv qui sert à indiquer quel morceau de la texture utiliser pour la face,
      les cordonnées vont [0, 0] à [16, 16], si la position de la texture n’est pas indiqué alors sera basée sur la position du cube (de l’élément).
      Si je veux que mon petit cube ressemble à un petit bloc de stone :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "faces": {
                  "down": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "up": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "north": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "south": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "west": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "east": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  }
              }
          }]
      }
      

      Un autre tag optionnel est cullface qui indique si la face ne doit pas être rendu si un bloc touche la face indiqué, par exemple avec le code suivant
      si un bloc touche la face du bas, la face du haut ne sera plus rendue.

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "faces": {
                  "down": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "up": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16],
                      "cullface": "down"
                  },
                  "north": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "south": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "west": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "east": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  }
              }
          }]
      }
      

      Le tag rotation permet de faire tourner la texture, la valeur doit être 0, 90, 180 ou 270. Dans le code suivant, la texture effectue on rotation de
      90° sur la face du haut :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "faces": {
                  "down": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "up": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16],
                      "rotation": 90
                  },
                  "north": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "south": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "west": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "east": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  }
              }
          }]
      }
      

      Il existe une autre tag qui s’appelle tintindex mais qui ne sert que pour quelques blocs, tels que la grass, les leaves, etc … Pour les autres
      il n’a aucun effet, il se place au même endroit que rotation ou cullface. Voilà c’est tout pour le tag faces.

      D’autres tags existent pour les éléments, on a déjà from, to et faces. On peut ajouter le tag shade pour indiquer si le bloc doit
      rendre une ombre, si il n’est spécifié alors la valeur true lui sera associée.
      Maintenant intéressons-nous au tag rotation :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "rotation": {
      
              },
              "faces": {
                  "down": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "up": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "north": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "south": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "west": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "east": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  }
              }
          }]
      }
      

      Dans ce tag rotation on va indiquer la rotation de l’élément, il faut tout d’abord annoncé le centre de rotation avec le tag origin, puis
      indiquer l’axe de rotation avec le tag axis, viens ensuite l’angle avec angle qui accepte les valeurs -45/-22.5/0/22.5/45 et enfin le
      tag rescale (facultatif, sur false par défaut) qui indique si il faut redimensionner le cube à l’échelle après sa rotation, c’est à dire
      qu’il aura la même distance avec les bords du cube qu’avant sa rotation (déforme le cube).
      Ici nous faisons tourner notre cube de 45 sur l’axe y avec une remise à l’échelle :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "rotation": {
                  "origin": [8, 8, 8],
                  "axis": "y",
                  "angle": 45,
                  "rescale": true
              },
              "faces": {
                  "down": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "up": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "north": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "south": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "west": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "east": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  }
              }
          }]
      }
      

      C’est tout pour les éléments, maintenant je vais vour parler du tag display qui va définir la façon de rendre le bloc quand il n’est pas posé
      (dans le gui, dans les item frames, au sol, dans la main (première et troisième personne), sur le tête).
      Tout d’abord le il faut ajouter le tag display :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "rotation": {
                  "origin": [8, 8, 8],
                  "axis": "y",
                  "angle": 45,
                  "rescale": true
              },
              "faces": {
                  "down": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "up": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "north": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "south": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "west": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "east": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  }
              }
          }],
          "display": {
      
          }
      }
      

      Dans ce tag, nous allons définir la dimension, la rotation, et la translation de l’objet pour les tags suivants :
      thirdperson_righthand, thirdperson_lefthand, firstperson_righthand, firstperson_lefthand, gui, head, ground, fixed
      Vous pouvez définir seulement le tag de la main droite, et la main gauche appliquera les changements en miroir.
      Attention : Les changements à la première personne n’influent pas sur la troisième personne.
      Les tags à mettre sont rotation, translation, scale.
      Je fait fait une exemple avec des valeurs aléatoires pour montrer où placer le code :

      {
          "textures": {
              "stone": "blocks/stone",
              "particle": "blocks/stone"
          },
          "elements": [{
              "from": [6, 6, 6],
              "to": [10, 10, 10],
              "rotation": {
                  "origin": [8, 8, 8],
                  "axis": "y",
                  "angle": 45,
                  "rescale": true
              },
              "faces": {
                  "down": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "up": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "north": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "south": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "west": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  },
                  "east": {
                      "texture": "#stone",
                      "uv": [0, 0, 16, 16]
                  }
              }
          }],
          "display": {
              "gui": {
                  "rotation": [-5, 45, 10],
                  "translation": [2, -2, 8],
                  "scale": [1.1, 1.2, 1.1]
              },
              "fixe": {
                  "rotation": [-5, 45, 10],
                  "translation": [2, -2, 8],
                  "scale": [0.9, 1.0, 0.9]
              }
          }
      }
      

      Maintetant je vais parler du tag parent, il indique quel modèle utiliser, par exemple si plusieurs blocs ont les mêmes éléments mais pas les mêmes
      textures, on va créer un fichier dans lequel tout les éléments seront définis puis pour chaque bloc ont définira ce bloc comme parent et on écrira le tag
      textures. Ce système correspond à l’héritage, si vous ré-écrivait un tag, il va override celui du parent.
      Ce tag est très utilisé pour faire différents blocs, par exemple le fichier blocks/cube_all défini un bloc de 16x16x16 avec la même texture sur toute
      les faces qui s’appelle all, donc pour faire ce type de bloc il faudra ce code :

      {
          "parent": "block/cube_all",
          "textures": {
              "all": "modid:blocks/nomDeLaTexture"
          }
      }
      

      Voici à quoi ressemble le fichier cube_all.json :

      {
          "parent": "block/cube",
          "textures": {
              "particle": "#all",
              "down": "#all",
              "up": "#all",
              "north": "#all",
              "east": "#all",
              "south": "#all",
              "west": "#all"
          }
      }
      

      Et le fichier cube.json

      {
          "parent": "block/block",
          "elements": [
              {   "from": [ 0, 0, 0 ],
                  "to": [ 16, 16, 16 ],
                  "faces": {
                      "down":  { "texture": "#down", "cullface": "down" },
                      "up":    { "texture": "#up", "cullface": "up" },
                      "north": { "texture": "#north", "cullface": "north" },
                      "south": { "texture": "#south", "cullface": "south" },
                      "west":  { "texture": "#west", "cullface": "west" },
                      "east":  { "texture": "#east", "cullface": "east" }
                  }
              }
          ]
      }
      

      Et enfin le fichier block.json

      {
          "display": {
              "gui": {
                  "rotation": [ 30, 225, 0 ],
                  "translation": [ 0, 0, 0],
                  "scale":[ 0.625, 0.625, 0.625 ]
              },
              "ground": {
                  "rotation": [ 0, 0, 0 ],
                  "translation": [ 0, 3, 0],
                  "scale":[ 0.25, 0.25, 0.25 ]
              },
              "fixed": {
                  "rotation": [ 0, 0, 0 ],
                  "translation": [ 0, 0, 0],
                  "scale":[ 0.5, 0.5, 0.5 ]
              },
              "thirdperson_righthand": {
                  "rotation": [ 75, 45, 0 ],
                  "translation": [ 0, 2.5, 0],
                  "scale": [ 0.375, 0.375, 0.375 ]
              },
              "firstperson_righthand": {
                  "rotation": [ 0, 45, 0 ],
                  "translation": [ 0, 0, 0 ],
                  "scale": [ 0.40, 0.40, 0.40 ]
              },
              "firstperson_lefthand": {
                  "rotation": [ 0, 225, 0 ],
                  "translation": [ 0, 0, 0 ],
                  "scale": [ 0.40, 0.40, 0.40 ]
              }
          }
      }
      

      Comme vous le voyez il y a une hiérarchie qui se construit. Il y a d’autres fichiers qui héritent de cube.json, nous avons les suivants :

      • cube_bottom_top : Il faut définir les textures side, top, bottom
      • cube_column : Il faut définir les textures side, end, cette dernière est la textures en haut et en bas du bloc
      • cube_top : Il faut définir les textures side, top
        D’autres fichiers JSON peuvent vous être utiles :
      • flower_pot_cross : Il faut définir la texture plant, permet d’avoir une fleur dans un pot.
      • torch et torch_wall : Il faut défnir la texture torch
      • trapdoor_bottom, trapdoor_top et trapdoor_open : Pour chacun il faut définir la texture texture
      • cross : Il faut défnir la texture cross, permet de faire des plantes
      • crop : Il faut définir la texture crop, permet de faire des plantations
      • carpet : Il faut définir la texture wool et particule
      • button, button_pressed, button_inventory : Il faut définir la texture texture
      • anvil : Il faut définir les textures particle, body, top
      • fence_inventory, fence_post, fence_side : Il faut définir la texture texture
      • fence_gate_closed, fence_gate_open : Il faut définir la texture texture
        Cette liste n’est pas exhaustive.

      Les modèles des objets

      Les modèles des objets ressemblent beaucoup à ceux des blocs. Je vais tout d’abord tout d’abord parler du nom du fichier JSON et de où le placer.
      Ce fichier doit se trouver dans le package assets.modid.models.item et doit avoir pour nom celui donné lors de l’enregistrement de l’objet dans le jeu et
      le l’enregistrement du modèle.

      Nous allons commencer par parler du tag parent cette fois-ci, si vous voulez juste afficher une texture comme c’est le cas pour beaucoup d’objets dans
      Minecraft (redstone, nourriture, lit, etc …) il vous faut alors utiliser le fichier generated avec comme texture layer0 :

      {
          "parent": "item/generated",
          "textures": {
              "layer0": "modid:items/yourTexture"
          }
      }
      

      Des modèles comme handheld et handheld_rod s’occupent de positionner l’objet dans votre main, il vous suffit de les défnir comme parent.

      Si vous voulez que votre objet soit un bloc vous pouvez utiliser comme parent le modèle de votre bloc, et oui, comme je l’ai dit ils se ressemblent. En fait
      les modèles des objets ont plus de tags disponibles que ceux des blocs, mais sinon ils possèdent les mêmes tags vous pouvez donc appliquer tout ce que j’ai
      dit précédemment pour les modèles des blocs avec les modèles des objets.
      Exemple d’utilisation du modèle d’un bloc comme parent pour un modèle d’objet :

      {
          "parent": "modid:block/modeleDeVotreBloc"
      }
      

      Ici aussi si vous re-définissez un tag, alors ce dernier override celui du parent. Je ne vais expliquer que les nouveaux tags, les autres n’ont pas changés.
      Un nouveau tag est overrides, il permet de diriger vers un autre modèle (il aiguille comme le fichier d’état de bloc) en fonction des propriétés de l’objet
      (par exemple : la canne à pêche possède la propriété cast, l’arc possède les propriétés pull et pulling).

      Ces propriétés sont ajoutées dans le constructeur de l’objet avec la fonction Item#addPropertyOverride(ResourceLocation key, IItemPropertyGetter getter)
      L’interface IItemPropertyGetter implémente la fonction public float apply(ItemStack stack, @Nullable World worldIn, @Nullable EntityLivingBase entityIn)
      Comme on peut le voir cette fonction renvoie un float en fonction des paramètres, c’est cette valeur qu’il faudra tester dans le JSON.
      Par exemple si nous avons cela dans le constructeur :

      this.addPropertyOverride(new ResourceLocation("held"), new IItemPropertyGetter()
          {
              @SideOnly(Side.CLIENT)
              public float apply(ItemStack stack, @Nullable World world, @Nullable EntityLivingBase entity)
              {
                  return entity == null ? 0.0F : 1.0F;
              }
          });
      

      Cette propriété renvoie 0 si l’item n’est pas dans l’inventaire du joueur et 1 si il y est, ainsi avec le code suivant :

      {
          "parent": "block/acacia_log",
          "overrides": [{
              "predicate": {
                  "held": 1
              },
              "model": "block/stone"
          }]
      }
      

      Quand l’objet est au sol c’est du bois et quand le joueur le ramasse c’est de la pierre (seulement côté visuel). Voilà comment le code se présente :
      Le tag overrides qui est un tableau de cas, chaque cas est entouré d’accolades, dans les accolades le tag predicate contient les conditions qui
      sont de la forme “key”: floatValue et enfin au même niveau que le tag predicate, on met le tag model qui contient le model vers lequel il
      faut se rendre.

      Bravo, vous savez à présent tout ce qu’il faut savoir sur les fichiers JSON de Minecraft. Vous ne direz plus “Oh, non la 1.8 et les jsons”

      Bonus

      Créer une barrière

      Nous allons voir comment créer une nouvelle barrière, ici je vais créer une barrière en pierre. Je vais tout simplement appliquer la texture
      de la pierre au modèle de la barrière.

      Première on va déclarer notre bloc :

      public static final Block stoneFence = new BlockFence(Material.ROCK, MapColor.STONE).setUnlocalizedName("stone_fence").setRegistryName("stone_fence");
      

      Ici notre bloc est une barrière donc nous utilisons la classe BlockFence puis nous l’enregistrons :

      GameRegistry.<Block>register(stoneFence);
      

      Nous allons aussi déclarer notre ItemBlock car rappelons-le ils ne sont plus créés automatiquement en 1.9 :

      public static final Item itemBlockStoneFence = new ItemBlock(BlocksModTutoriel.stoneFence).setUnlocalizedName("stone_fence").setRegistryName("stone_fence");
      

      Et enregistrons-le :

      GameRegistry.register(itemBlockStoneFence);
      

      N’oubliez pas d’enregistrer son rendu, pour ma part j’ai une fonction ClientProxy#registerItemTexture :

      registerItemTexture(itemBlockStoneFence, "stone_fence");
      

      Voici ma fonction :

      public void registerItemTexture(Item item, int metadata, String name)
      {
          ItemModelMesher mesher = Minecraft.getMinecraft().getRenderItem().getItemModelMesher();
          mesher.register(item, metadata, new ModelResourceLocation(ModTutoriel.MODID + ":" + name, "inventory"));
      }
      

      Venons-en au centre du tutoriel, les fichiers JSON, commençons par le fichier d’état de bloc :
      assets/modid/blockstates/stone_fence.json

      {
          "multipart": [{
              "apply": {
                  "model": "modid:stone_fence_post"
              }
          }, {
              "when": {
                  "north": "true"
              },
              "apply": {
                  "model": "modid:stone_fence_side",
                  "uvlock": true
              }
          }, {
              "when": {
                  "east": "true"
              },
              "apply": {
                  "model": "modid:stone_fence_side",
                  "y": 90,
                  "uvlock": true
              }
          }, {
              "when": {
                  "south": "true"
              },
              "apply": {
                  "model": "modid:stone_fence_side",
                  "y": 180,
                  "uvlock": true
              }
          }, {
              "when": {
                  "west": "true"
              },
              "apply": {
                  "model": "modid:stone_fence_side",
                  "y": 270,
                  "uvlock": true
              }
          }]
      }
      

      Comme vous pouvez le voir on utilise le multipart, il faut savoir que les propriétés north, west, south, east valent “true” si la barrière peut se lier
      au bloc qui se trouve dans cette direction. La barrière se compose de deux modèles, le pilier central et la jointure à laquelle on appliquer une rotation suivant
      la direction dans laquelle elle doit pointer. La pilier est toujours affiché, d’où l’absence de tag when pour ce dernier. Ensuite pour chaque côté on
      regarde si il y a un bloc auquel il faut lier et si c’est le cas on affiche le modèle de la jointure avec la rotation appropriée.

      On maintenant créer les modèles stone_fence_post et stone_fence_side :
      assets/modid/models/block/stone_fence_post.json

      {
          "parent": "block/fence_post",
          "textures": {
              "texture": "blocks/stone"
          }
      }
      

      assets/modid/models/block/stone_fence_side.json

      {
          "parent": "block/fence_side",
          "textures": {
              "texture": "blocks/stone"
          }
      }
      

      Grâce au système d’héritage nous n’avons pas besoin de définir les éléments composants notre barrière, il faut seulement dire quelle texture utiliser.
      Pour le modèle l’objet de la barrière on va utiliser le fichier block/fence_inventory auquel nous allons appliquer la texture de la pierre.
      assets/modid/models/item/stone_fence.json

      {
          "parent": "block/fence_inventory",
          "textures": {
              "texture": "blocks/stone"
          }
      }
      

      Voilà, pour pouvez maintenant utiliser une barrière toute belle, toute neuve et surtout solide.

      Créer un modèle pour les tuyaux/câbles de vos mods

      Pour créer un câble nous allons procéder de la même manière que Minecraft lorsqu’il relie les barrière ensembles, cepedant nous allons rajouter les
      directions haut et bas, en plus de nord, sud, est, ouest.

      Nous allons commencer par créer la classe du bloc, elle héritera de classe Block. On va y ajouter 6 PropertyBool qui correspondront aux 6
      faces du bloc, si la propriété vaut vrai, alors c’est qu’il est relié au bloc dans cette direction.
      blocks/BlockPipe.java

      public class BlockPipe extends Block {
      
          public static final PropertyBool NORTH = PropertyBool.create("north");
          public static final PropertyBool EAST = PropertyBool.create("east");
          public static final PropertyBool SOUTH = PropertyBool.create("south");
          public static final PropertyBool WEST = PropertyBool.create("west");
          public static final PropertyBool DOWN = PropertyBool.create("down");
          public static final PropertyBool UP = PropertyBool.create("up");
      
          public BlockPipe(Material material)
          {
              super(material);
              this.setDefaultState(this.blockState.getBaseState()
              .withProperty(NORTH, false)
              .withProperty(EAST, false)
              .withProperty(SOUTH, false)
              .withProperty(WEST, false)
              .withProperty(DOWN, false)
              .withProperty(UP, false)
              );
          }
      }
      

      Comme vous pouvez le voir, on a créé un propriété par face puis dans le constructeur du bloc nous avons définit l’état du bloc par défaut, c’est à dire
      qu’il n’est relié à aucun bloc par défaut. Notre bloc ne sera pas un bloc plein, donc il faut le spécifier.
      blocks/BlockPipe.java

      public boolean isFullCube(IBlockState state)
      {
          return false;
      }
      
      public boolean isPassable(IBlockAccess worldIn, BlockPos pos)
      {
          return false;
      }
      
      @SideOnly(Side.CLIENT)
      public boolean shouldSideBeRendered(IBlockState blockState, IBlockAccess blockAccess, BlockPos pos, EnumFacing side)
      {
          return true;
      }
      

      Comme nous avons créer des propriétés nous devons dire à Minecraft que le bloc les utilise et comment convertir les états en métadata sinon on aura un crash. Dans notre cas, le métadata ne dépendra pas des propriétés.
      blocks/BlockPipe.java

      public int getMetaFromState(IBlockState state)
      {
          return 0;
      }
      
      protected BlockStateContainer createBlockState()
      {
          return new BlockStateContainer(this, new IProperty[] {NORTH, EAST, WEST, SOUTH, DOWN, UP});
      }
      

      Ensuite il va falloir créer un fonction pour savoir si notre bloc doit se relier sur tel, ou tel côté.
      blocks/BlockPipe.java

      public boolean canConnectTo(IBlockAccess worldIn, BlockPos pos)
      {
          return worldIn.getBlockState(pos).getBlock() instanceof BlockPipe;
      }
      

      Ici je vérifie si le bloc à la position donnée est un tuyau, j’ai donc instanceof BlockPipe, si vous voulez que le tuyau se relie à d’autres blocs
      vous pouvez simplement rajouter des conditions sépararée par des OU. Si vous avez un grand nombre de blocs auquels votre tuyau peut se relier vous pouvez
      créer une interface que vous implémenterez à ces blocs et votre fonction ressemblera à ça :

      public boolean canConnectTo(IBlockAccess worldIn, BlockPos pos)
      {
          return worldIn.getBlockState(pos).getBlock() instanceof VotreInterface;
      }
      

      Et donc pour finir la classe du bloc on va changer la fonction getActualState pour changer les propriétés en fonction du retour de la fonction
      prédédemment créée.
      blocks/BlockPipe.java

      public IBlockState getActualState(IBlockState state, IBlockAccess worldIn, BlockPos pos)
      {
          return state.withProperty(NORTH, this.canConnectTo(worldIn, pos.north()))
          .withProperty(EAST, this.canConnectTo(worldIn, pos.east()))
          .withProperty(SOUTH, this.canConnectTo(worldIn, pos.south()))
          .withProperty(WEST, this.canConnectTo(worldIn, pos.west()))
          .withProperty(DOWN, this.canConnectTo(worldIn, pos.down()))
          .withProperty(UP,this.canConnectTo(worldIn, pos.up()));
      }
      

      C’est tout pour le bloc. Pensez bien à le déclarer :

      public static final Block blockPipe = new BlockPipe(Material.ROCK).setUnlocalizedName("blockPipe").setRegistryName("blockPipe");
      

      Et à l’enregistrer :

      GameRegistry.<block>register(blockPipe);
      

      Passons à présent aux JSONs, commençons par le JSON d’état du bloc, le bloc votre être composé de deux modèles, le centre qui sera rendu quoi qu’il arrive,
      et le bord que l’on rendra autant de fois qu’il faut et auquel on appliquera une rotation.
      assets/modid/blockstates/blockPipe.json

      {
          "multipart": [
              {   "apply": { "model": "modid:blockPipe_center" }},
              {   "when": { "north": "true" },
                  "apply": { "model": "modid:blockPipe_side", "uvlock": true }
              },
              {   "when": { "east": "true" },
                  "apply": { "model": "modid:blockPipe_side", "y": 90, "uvlock": true }
              },
              {   "when": { "south": "true" },
                  "apply": { "model": "modid:blockPipe_side", "y": 180, "uvlock": true }
              },
              {   "when": { "west": "true" },
                  "apply": { "model": "modid:blockPipe_side", "y": 270, "uvlock": true }
              },
              {   "when": { "down": "true" },
                  "apply": { "model": "modid:blockPipe_side", "x": 90, "uvlock": true }
              },
              {   "when": { "up": "true" },
                  "apply": { "model": "modid:blockPipe_side", "x": 270, "uvlock": true }
              }
          ]
      }
      

      Comme vous pouvez le voir, on a le modèle blockPipe_center qui sera rendu quoi qu’il arrive. Puis suivant l’état des propriétés on rend le
      modèle blockPipe_side avec un rotation.

      Ces modèles sont les suivants, premièrement le centre, qui est un bloc plus petit.
      assets/modid/models/block/blockPipe_center.json

      {
          "textures": {
              "texture": "modid:blocks/basicTexture",
              "particle": "modid:blocks/basicTexture"
          },
          "elements": [
              {   "from": [ 6, 6, 6 ],
                  "to": [ 10, 10, 10 ],
                  "faces": {
                      "down":  { "texture": "#texture" },
                      "up":    { "texture": "#texture" },
                      "north": { "texture": "#texture" },
                      "south": { "texture": "#texture" },
                      "west":  { "texture": "#texture" },
                      "east":  { "texture": "#texture" }
                  }
              }
          ]
      }
      

      Et deuxièmement le modèle qui relie deux blocs, c’est un petit pavé situé sur le côté du bloc.
      assets/modid/models/block/blockPipe_side.json

      {
          "textures": {
              "texture": "modid:blocks/basicTexture",
              "particle": "modid:blocks/basicTexture"
          },
          "elements": [
              {   "from": [ 6, 6, 0 ],
                  "to": [ 10, 10, 6 ],
                  "faces": {
                      "down":  { "texture": "#texture" },
                      "up":    { "texture": "#texture" },
                      "north": { "texture": "#texture" },
                      "south": { "texture": "#texture" },
                      "west":  { "texture": "#texture" },
                      "east":  { "texture": "#texture" }
                  }
              }
          ]
      }
      

      Pour faire l’ItemBlock, c’est comme pour un bloc normal, je ne vais pas le refaire ici.

      Voici le rendu :

      Résultat

      Montrez va plus beau résultats dans le réponses sous le sujet et laissez libre cours à votre imagination.

      posted in Les blocs
      BrokenSwing
      BrokenSwing
    • RE: Problème avec l'implémentation de l'API chisel

      Que l’on m’arrête si je me trompe mais il me semble que tu t’y prend mal. J’ai déjà utilisé l’API de chisel et ce n’est pas comme cela que je m’y suis pris. Télécharge ceci https://minecraft.curseforge.com/projects/chisel/files/2465086 et ajoute le à ton classpath (dans la partie depedencies de ton build.gradle), tu relance le gradlew eclipse puis tu aura accès à l’API normalement. A partir d’ici tu n’utilises que l’API. Chisel utilise l’InterModCommunication de Forge

      posted in 1.12.x
      BrokenSwing
      BrokenSwing
    • RE: Monture Custom

      Pour ce qui est du fait que le constructeur Vec3 qui n’existe pas, il suffit de faire un CTRL+CLIC sur Vec3 pour entrer dans la classe et voir qu’il faut utiliser

      Vec3.createVectorHelper(double, double, double);
      

      Ce qui retourne une instance de Vec3.

      Ce que te propose c’est ça :

      
      public boolean itemInteractionForEntity(ItemStack stack, EntityPlayer player, EntityLivingBase entity)
      {
      if (entity instanceof EntityCreeper)
      {
      entity.setDead();
      –stack.stackSize;
      player.inventory.addItemStackToInventory(new ItemStack(Items.baked_potato));
      return true;
      }
      else
      {
      return false;
      }
      }
      
      
      posted in Sans suite
      BrokenSwing
      BrokenSwing
    • 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
      • Code
      • Résultat
      • Crédits

      Pré-requis

      • Créer la base d’un mod

      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

      posted in Autres
      BrokenSwing
      BrokenSwing
    • [1.12] Créer un four

      Sommaire

      • Introduction
      • Pré-requis
      • Code
        • La classe principale
        • La classe du bloc
        • La classe du TileEntity
        • La classe des recettes
        • La classe du container
        • La classe du GUI
      • Résultat
      • Crédits

      Introduction

      Nous allons créer un bloc qui prend des items en entrée et qui sort d’autres items en sortie comme à la façon d’un four par exemple.
      Pour cet exemple j’ai choisi un bloc qui prendra 2 objets en entrée, 2 objets pour l’alimenter et qui sortira 1 objet.
      Je vous montrerez cependant comment personnaliser le code pour obtenir ce que vous voulez mais pour cela il va falloir
      bien comprendre le code donc soyez attentif.

      Pré-requis

      • Créer un bloc basique
      • Créer un gui et un container sur un bloc OUTDATED
      • Ajouter un TileEntity au bloc OUTDATED

      Code

      La classe principale

      On va commencer par déclarer notre bloc, dans ce tutoriel je l’appellerai custom_furnace

      public static final Block CUSTOM_FURNACE = new BlockCustomFurnace().setRegistryName("modid:custom_furnace");
      

      Pensez à respecter la convention Java, cette variable est static et final, utilisez donc le SNAKE_CASE.

      Créez la classe BlockCustomFurnace.

      Enregistrez alors votre bloc :

      @SubscribeEvent
      public static void registerBlocks(RegistryEvent.Register<Block> event) {
          event.getRegistry().register(ModBlocks.CUSTOM_FURNACE);
      }
      

      Dans la méthode preInit de votre classe principale, enregistrez votre TileEntity :

      GameRegistry.registerTileEntity(TileCustomFurnace.class, "modid:tile_custom_furnace");
      

      Créez la classe TileCustomFurnace.

      Il faut aussi enregistrer le GUI Handler, mais ça devrait déjà être fait grâce aux pré-requis :

      NetworkRegistry.INSTANCE.registerGuiHandler(instance, new GuiHandler());
      

      Voilà pour la classe principale.

      La classe du bloc

      Allez maintenant dans la classe du bloc et faite-la hériter à BlockContainer :

      public class BlockCustomFurnace extends BlockContainer
      

      Ajoutez le constructeur :

      public BlockCustomFurnace() {
          super(Material.rock); // Choisissez ce que que vous voulez
          // Autres paramètres
      }
      

      On va maintenant déclarer que le bloc possède un TileEntity et dire quel TileEntity créer :

      @Override
      public boolean hasTileEntity() {
          return true;
      }
      
      @Override
      public TileEntity createNewTileEntity(World world, int metadata)  {
          return new TileCustomFurnace();
      }
      

      Maintenant occupons-nous de la méthode qui va permettre de drop les items quand on casse le bloc :

      @Override
      public void breakBlock(World worldIn, BlockPos pos, IBlockState state) {
          TileEntity tileentity = worldIn.getTileEntity(pos);
      
          if (tileentity instanceof TileCustomFurnace) {
              InventoryHelper.dropInventoryItems(worldIn, pos,
                      (TileCustomFurnace) tileentity);
          }
      
          super.breakBlock(worldIn, pos, state);
      }
      

      Et maintenant la méthode pour ouvrir le gui lorsqu’on fait clique droit sur le bloc :

      @Override
      public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ) {
          if (world.isRemote) {
              return true;
          } else {
              TileEntity tileentity = world.getTileEntity(pos);
      
              if (tileentity instanceof TileCustomFurnace) {
                  player.openGui(ModTuto.instance, 0, world, pos.getX(),
                          pos.getY(), pos.getZ());
              }
      
              return true;
          }
      }
      

      Pour que votre bloc soit rendu comme un bloc normal il va falloir Override la fonction getRenderType :

      @Override
      public EnumBlockRenderType getRenderType(IBlockState state) {
          return EnumBlockRenderType.MODEL;
      }
      

      Et ceci pour mettre un nom personnalisé au bloc une fois posé :

      @Override
      public void onBlockPlacedBy(World worldIn, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack) {
          if (stack.hasDisplayName()) {
              TileEntity tileentity = worldIn.getTileEntity(pos);
      
              if (tileentity instanceof TileCustomFurnace) {
                  ((TileCustomFurnace) tileentity).setCustomName(stack
                          .getDisplayName());
              }
          }
      }
      

      Vous avez sûrement des errreurs, ignorez-les, nous n’avons pas encore créé les différentes méthodes.

      Voilà pour la classe du bloc.

      La classe du TileEntity

      Allez dans la classe du TileCustomFurnace.

      On va déclarer plusieurs variables

      private NonNullList<ItemStack> stacks = NonNullList.withSize(5, ItemStack.EMPTY);
      private String customName;
      private int	timePassed = 0;
      private int	burningTimeLeft	= 0;
      

      Quelques explications :

      • customName contient le nom personnalisé du bloc si il en a un
      • stacks contient les ItemStack de votre bloc autrement dit tout les slots, c’est ici que sont stockés les items
      • timePassed contient l’avancement de la recette, il représente le temps passé
      • burningTimeLeft contient le temps restant avant avant qu’il n’y est plus de feux
        Maintenant faisont hériter la classe à TileEntityLockable et implémentons ITickable
      public class TileCustomFurnace extends TileEntityLockable implements ITickable
      

      N’implémentez pas les méthodes, on va le faire à la main.

      Tout d’abord les méthodes pour lire et écrire dans les NBT, dedans on lit, on écrit, rien de sorcier quand on sait utiliser les NBT :

      @Override
      public void readFromNBT(NBTTagCompound compound) {
          super.readFromNBT(compound);
          this.stacks = NonNullList.<ItemStack>withSize(this.getSizeInventory(), ItemStack.EMPTY);
          ItemStackHelper.loadAllItems(compound, this.stacks);
      
          if (compound.hasKey("CustomName", 8)) {
              this.customName = compound.getString("CustomName");
          }
          this.burningTimeLeft = compound.getInteger("burningTimeLeft");
          this.timePassed = compound.getInteger("timePassed");
      }
      
      @Override
      public NBTTagCompound writeToNBT(NBTTagCompound compound) {
          super.writeToNBT(compound);
          ItemStackHelper.saveAllItems(compound, this.stacks);
      
          if (this.hasCustomName()) {
              compound.setString("CustomName", this.customName);
          }
      
          compound.setInteger("burningTimeLeft", this.burningTimeLeft);
          compound.setInteger("timePassed", this.timePassed);
      
          return compound;
      }
      

      La fonction hasCustomName n’existe pas, nous allons la créer et tout ce qui va avec :

      @Override
      public boolean hasCustomName() {
          return this.customName != null && !this.customName.isEmpty();
      }
      
      @Override
      public String getName() {
          return hasCustomName() ? this.customName : "tile.custom_furnace";
      }
      
      public void setCustomName(String name) {
          this.customName = name;
      }
      

      Nous allons à présent créer les fonctions qui vont permettre d’accéder aux variables burningTimeLeft et timePassed :

      @Override
      public int getField(int id) {
          switch (id) {
              case 0:
                  return this.burningTimeLeft;
              case 1:
                  return this.timePassed;
          }
          return 0;
      }
      
      @Override
      public void setField(int id, int value) {
          switch (id) {
              case 0:
                  this.burningTimeLeft = value;
                  break;
              case 1:
                  this.timePassed = value;
          }
      }
      
      @Override
      public int getFieldCount() {
          return 2;
      }
      

      Ce sont juste des getters et setters.

      Maintenant, créons les fonctions qui permettrons de manipuler les ItemStack de nos slots :

      @Override
      public int getSizeInventory() {
          return this.stacks.size();
      }
      
      @Override
      public ItemStack getStackInSlot(int index) {
          return this.stacks.get(index);
      }
      
      @Override
      public ItemStack decrStackSize(int index, int count) {
          return ItemStackHelper.getAndSplit(this.stacks, index, count);
      }
      
      @Override
      public ItemStack removeStackFromSlot(int index) {
          return ItemStackHelper.getAndRemove(stacks, index);
      }
      
      @Override
      public void setInventorySlotContents(int index, ItemStack stack) {
          this.stacks.set(index, stack);
      
          if (stack.getCount() > this.getInventoryStackLimit()) {
              stack.setCount(this.getInventoryStackLimit());
          }
      }
      
      @Override
      public int getInventoryStackLimit() {
          return 64;
      }
      
      @Override
      public boolean isEmpty() {
          for(ItemStack stack : this.stacks) {
              if (!stack.isEmpty()) {
                  return false;
              }
          }
          return true;
      }
      
      @Override
      public void clear() {
          for(int i = 0; i < this.stacks.size(); i++) {
              this.stacks.set(i, ItemStack.EMPTY);
          }
      }
      

      Toutes ces fonctions sont très simples, je ne vais pas les détaillés, l’interface IInventory indique de toute façon leur utilité.
      Rajoutez aussi ces deux fonctions qui seront appelées lors de l’ouverture et de la fermeture de l’inventaire :

      @Override
      public void openInventory(EntityPlayer player) {}
      
      @Override
      public void closeInventory(EntityPlayer player) {}
      

      Rajoutez aussi ceux deux autres fonctions qui ne sont utiles qu’à Minecraft :

      @Override
      public Container createContainer(InventoryPlayer playerInventory, EntityPlayer playerIn) {
          return null;
      }
      
      @Override
      public String getGuiID() {
          return null;
      }
      

      Maintenant nous allons définir ce que peut contenir chaque slot (cette fonction ne sert que pour l’automatisation, pas pour le GUI) :

      @Override
      public boolean isItemValidForSlot(int index, ItemStack stack) {
          // Le slot 3 je n'autorise que les planches de bois
          if (index == 2)
              return OreDictionary.getOres("plankWood").contains(
                      new ItemStack(stack.getItem(), 1,
                              OreDictionary.WILDCARD_VALUE));
          // Le slot 4 je n'autorise que le blé
          if (index == 3)
              return stack.getItem() == Items.WHEAT;
          // Le slot 5 (celui du résultat) je n'autorise rien
          if (index == 4)
              return false;
          // Sinon pour les slots 1 et 2 on met ce qu'on veut
          return true;
      }
      

      Il nous faut aussi une fonction qui sera appelée depuis le Container pour savoir si le joueur peut utiliser l’inventaire :

      /** Vérifie la distance entre le joueur et le bloc et que le bloc soit toujours présent */
      public boolean isUsableByPlayer(EntityPlayer player) {
          return this.world.getTileEntity(this.pos) != this ? false : player
                  .getDistanceSq((double) this.pos.getX() + 0.5D,
                          (double) this.pos.getY() + 0.5D,
                          (double) this.pos.getZ() + 0.5D) <= 64.0D;
      }
      

      Maintenant nous allons nous occuper du processus de cuisson, afin de nous aider nous allons créer quelques fonctions :

      public boolean hasFuelEmpty() {
          return this.getStackInSlot(2).isEmpty()
                  || this.getStackInSlot(3).isEmpty();
      }
      

      Permet de savoir si une slot de carburant est vide.

      public ItemStack getRecipeResult() {
          return RecipesCustomFurnace.getRecipeResult(new ItemStack[] {
                  this.getStackInSlot(0), this.getStackInSlot(1) });
      }
      

      La classe RecipesCustomFurnace n’existe toujours pas, c’est la partie d’après. Cette fonction sert à récupérer la recette associée aux ingrédients.

      public boolean canSmelt() {
          // On récupère le résultat de la recette
          ItemStack result = this.getRecipeResult();
      
          // Le résultat est null si il n'y a pas de recette associée, donc on retourne faux
          if (result != null) {
      
              // On récupère le contenu du slot de résultat
              ItemStack slot4 = this.getStackInSlot(4);
      
              // Si il est vide on renvoie vrai
              if (slot4.isEmpty())
                  return true;
      
              // Sinon on vérifie que ce soit le même objet, les même métadata et que la taille finale ne sera pas trop grande
              if (slot4.getItem() == result.getItem() && slot4.getItemDamage() == result.getItemDamage()) {
                  int newStackSize = slot4.getCount() + result.getCount();
                  if (newStackSize <= this.getInventoryStackLimit() && newStackSize <= slot4.getMaxStackSize()) {
                      return true;
                  }
              }
          }
          return false;
      }
      

      Cette fonction renvoie vrai si on peut faire cuire les ingrédients, c’est à dire que les ingrédients sont bons et que le résultat de la recette
      peut être mis dans le slot du résultat.

      Nous allons à présent rajouter la fonction qui fait cuire les ingrédients (qui transforme les ingrédient en résultat de la recette) :

      public void smelt() {
          // Cette fonction n'est appelée que si result != null, c'est pourquoi on ne fait pas de null check
          ItemStack result = this.getRecipeResult();
          // On enlève un item de chaque ingrédient
          this.decrStackSize(0, 1);
          this.decrStackSize(1, 1);
          // On récupère le slot de résultat
          ItemStack stack4 = this.getStackInSlot(4);
          // Si il est vide
          if (stack4.isEmpty()) {
              // On y insère une copie du résultat
              this.setInventorySlotContents(4, result.copy());
          } else {
              // Sinon on augmente le nombre d'objets de l'ItemStack
              stack4.setCount(stack4.getCount() + result.getCount());
          }
      }
      

      Et trois dernière fonctions auxiliaires pour nous aider :

      /** Temps de cuisson de la recette */
      public int getFullRecipeTime() {
          return 200;
      }
      
      /** Temps que dure 1 unité de carburant (ici : 1 planche + 1 blé) */
      public int getFullBurnTime() {
          return 300;
      }
      
      /** Renvoie vrai si le feu est allumé */
      public boolean isBurning() {
          return burningTimeLeft > 0;
      }
      

      Nous allons implémenter notre toute dernière fonction, celle de ITickable :

      @Override
      public void update() {
      
      }
      

      Elle est appelée à chaque tick. Tout ce qui se trouve dans la fonction devra être exécuté côté serveur donc :

      @Override
      public void update() {
          if (!this.world.isRemote) {
      
          }
      }
      

      Tout d’abord si le four est allumé on va diminuer le temps restant du feu :

      @Override
      public void update() {
          if (!this.world.isRemote) {
      
              /* Si le carburant brûle, on réduit réduit le temps restant */
              if (this.isBurning()) {
                  this.burningTimeLeft--;
              }
          }
      }
      

      Si le four n’est pas allumé, que le la recette est bonne et qu’il y a du carburant, alors on allume le four :

      @Override
      public void update() {
          if (!this.world.isRemote) {
      
              /* Si le carburant brûle, on réduit réduit le temps restant */
              if (this.isBurning()) {
                  this.burningTimeLeft--;
              }
      
              /*
                  * Si la on peut faire cuire la recette et que le four ne cuit pas
                  * alors qu'il peut, alors on le met en route
                  */
              if (!this.isBurning() && this.canSmelt() && !this.hasFuelEmpty()) {
                  this.burningTimeLeft = this.getFullBurnTime();
                  this.decrStackSize(2, 1);
                  this.decrStackSize(3, 1);
              }
          }
      }
      

      Et maintenant si le four est allumé et que la recette est bonne, alors on augmente l’avancement de la recette. Si l’avancement de la recette est au maximum
      alors on cuit les ingrédients.

      @Override
      public void update() {
          if (!this.world.isRemote) {
      
              /* Si le carburant brûle, on réduit réduit le temps restant */
              if (this.isBurning()) {
                  this.burningTimeLeft--;
              }
      
              /*
                  * Si la on peut faire cuire la recette et que le four ne cuit pas
                  * alors qu'il peut, alors on le met en route
                  */
              if (!this.isBurning() && this.canSmelt() && !this.hasFuelEmpty()) {
                  this.burningTimeLeft = this.getFullBurnTime();
                  this.decrStackSize(2, 1);
                  this.decrStackSize(3, 1);
              }
      
              /* Si on peut faire cuire la recette et que le feu cuit */
              if (this.isBurning() && this.canSmelt()) {
                  this.timePassed++;
                  if (timePassed >= this.getFullRecipeTime()) {
                      timePassed = 0;
                      this.smelt();
                  }
              } else {
                  timePassed = 0;
              }
              this.markDirty();
          }
      }
      

      Voilà, à présent nous allons passer à la classe des recettes.

      La classe des recettes

      Créez la classe RecipesCustomFurnace si ce n’est pas fait. Déclarez la HashMap suivante :

      private static final HashMap <ItemStack[], ItemStack>recipes = new HashMap<ItemStack[], ItemStack>();
      static {
          addRecipe(Items.APPLE, Items.ARROW, Items.BAKED_POTATO);
      }
      

      Elle permettra de faire le lien entre les ingrédients et le résultat des recettes.
      Le scope static permet de rajouter une recette.

      Déclarons maintenant les fonctions pour ajouter les recettes :

      private static void addRecipe(Item ingredient1, Item ingredient2, Item resultat1) {
          addRecipe(new ItemStack(ingredient1), new ItemStack(ingredient2), new ItemStack(resultat1));
      }
      
      private static void addRecipe(ItemStack ingredient1, ItemStack ingredient2, ItemStack resultat1) {
          recipes.put(new ItemStack[]{ingredient1, ingredient2}, resultat1);
      }
      

      Créons aussi cette fonction pour comparer les ItemStack :

      private static boolean areKeysEqual(ItemStack[] key1, ItemStack[] key2) {
          if(key1.length != key2.length) return false;
      
          for(int i = 0; i < key1.length; i++) {
              ItemStack s1 = key1[i];
              ItemStack s2 = key2[i];
              if(s1.isEmpty() && !s2.isEmpty()) return false;
              if(!s1.isEmpty() && s2.isEmpty()) return false;
              if(s1.getItem() != s2.getItem()) return false;
              if(s1.getItemDamage() != s2.getItemDamage()) return false;
          }
          return true;
      }
      

      Et la fonction qui permet de trouver la recette :

      public static ItemStack getRecipeResult(ItemStack[] ingredients) {
          Iterator<Entry<ItemStack[], ItemStack>> it = recipes.entrySet().iterator();
          while(it.hasNext()) {
              Entry <ItemStack[], ItemStack>entry = it.next();
              if(areKeysEqual(entry.getKey(), ingredients)) {
                  return entry.getValue();
              }
          }
          return null;
      }
      

      La classe du container

      Créez la classe ContainerCustomFurnace, faites-la hériter de Container.
      Déclarer 3 variables :

      private TileCustomFurnace tile;
      private int	timePassed = 0;
      private int	burnTimeLeft = 0;
      

      On ajoute les slots au container dans le constructeur :

      public ContainerCustomFurnace(TileCustomFurnace tile, InventoryPlayer playerInventory) {
          this.tile = tile;
      
          int i;
          for(i = 0; i < 2; i++) {
              this.addSlotToContainer(new Slot(tile, i, 33 + i * 18, 7));
          }
          for(i = 0; i < 2; i++) {
              this.addSlotToContainer(new SlotSingleItem(tile, i + 2, 42, 40 + i * 18, i == 0 ? Item.getItemFromBlock(Blocks.PLANKS) : Items.WHEAT));
          }
          this.addSlotToContainer(new SlotOutput(tile, 4, 116, 17));
      
          for(i = 0; i < 3; ++i) {
              for(int j = 0; j < 9; ++j) {
                  this.addSlotToContainer(new Slot(playerInventory, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
              }
          }
      
          for(i = 0; i < 9; ++i) {
              this.addSlotToContainer(new Slot(playerInventory, i, 8 + i * 18, 142));
          }
      }
      

      Les slots SlotSingleItem et SlotOutput sont des slots créés par moi-même, on verra ça après.
      La focntion pour savoir si le joueur peut utiliser le containe :

      @Override
      public boolean canInteractWith(EntityPlayer player) {
          return tile.isUsableByPlayer(player);
      }
      

      Et les fonctions pour mettre à jour les valeurs du TileEntity pour l’affichage sur le client :

      @Override
      public void addListener(IContainerListener listener) {
          super.addListener(listener);
          listener.sendAllWindowProperties(this, this.tile);
      }
      
      @Override
      public void detectAndSendChanges() {
          super.detectAndSendChanges();
      
          for(int i = 0; i < this.listeners.size(); ++i) {
              IContainerListener icontainerlistener = (IContainerListener) this.listeners
                      .get(i);
      
              if (this.burnTimeLeft != this.tile.getField(0)) {
                  icontainerlistener.sendProgressBarUpdate(this, 0,
                          this.tile.getField(0));
              }
      
              if (this.timePassed != this.tile.getField(1)) {
                  icontainerlistener.sendProgressBarUpdate(this, 1,
                          this.tile.getField(1));
              }
          }
      
          this.burnTimeLeft = this.tile.getField(0);
          this.timePassed = this.tile.getField(1);
      }
      
      @Override
      @SideOnly(Side.CLIENT)
      public void updateProgressBar(int id, int data) {
          this.tile.setField(id, data);
      }
      

      Si vous êtes en 1.12+, la fonction sendProgressBarUpdate est devenue sendWindowProperty.

      Et la fonction pour gérer le shift+clic, étant donné que cette fonction est relative au nombre de slot choisis, etc … Je désactive ici le SHIFT + CLIC :

      @Override
      public ItemStack transferStackInSlot(EntityPlayer playerIn, int index) {
          return ItemStack.EMPTY;
      }
      

      On va créer nos deux classe de Slot :

      public class SlotOutput extends Slot {
      
          public SlotOutput(IInventory inventoryIn, int index, int xPosition, int yPosition) {
              super(inventoryIn, index, xPosition, yPosition);
          }
      
          @Override
          public boolean isItemValid(ItemStack stack) {
              return false;
          }
      }
      

      Et :

      public class SlotSingleItem extends Slot {
      
          private Item item;
      
          public SlotSingleItem(IInventory inventoryIn, int index, int xPosition, int yPosition, Item item) {
              super(inventoryIn, index, xPosition, yPosition);
              this.item = item;
          }
      
          @Override
          public boolean isItemValid(ItemStack stack) {
              return stack.isEmpty() || stack.getItem() == item;
          }
      }
      

      La classe du GUI

      Dernière étape, le GUI. Créez la classe GuiCustomFurnace et mettez-la dans le package client. Il faut étendre la classe à GuiContainer.

      public class GuiCustomFurnace extends GuiContainer
      

      Il faut ensuite déclarer deux variables :

      private static final ResourceLocation background = new ResourceLocation("modid","textures/gui/container/custom_furnace.png");
      private TileCustomFurnace tile;
      

      Explications :

      • texture : lien vers le fond du GUI
      • tile : TileEntity associé à ce GUI, sera le même que celui du container
        A présent mettez le constructeur suivant :
      public GuiCustomFurnace(TileCustomFurnace tile, InventoryPlayer playerInv) {
              super(new ContainerCustomFurnace(tile, playerInv));
              this.tile = tile;
      }
      

      Les deux fonctions suivantes permettent de dessiner le Gui :

      • drawGuiContainerBackgroundLayer permet de dessiner l’arrière plan
      • drawGuiContainerForegroundLayer permet de dessiner le permier plan

      Vous pouvez dessiner à l’aide des fonctions :

      this.drawTexturedModalRect(int x, int y, int textureX, int textureY, int width, int height)
      

      x correspond à la coordonnée x de l’endroit où vous voulez afficher votre texture
      y correspond à la coordonnée y de l’endroit où vous voulez afficher votre texture
      textureX correspond à la coordonnée x du morceau de texture que vous voulez afficher
      textureY correspond à la coordonnée y du morceau de texture que vous voulez afficher
      width correspond à largeur du morceau de texture que vous voulez afficher
      height correspond à la hauteur du morceau de texture que vous voulez afficher

      Quand vous utilisez cette fonction, il faut associer la texture au textureManager de minecraft, il faut
      donc mettre 1 fois au début de la fonction

      this.mc.getTextureManager().bindTexture(background);
      

      On peut écrire à l’aide de cette fonction

      this.fontRendererObj.drawString(String texte, int x, int, y, int color)
      

      texte est le texte que vous voulez afficher
      x est la coordonnée x de l’endroit où vous voulez l’afficher
      y est la coordonnée y de l’endroit où vous voulez l’afficher
      color est la couleur du texte

      Pour notre custom_furnace j’ai codé ceci :

      
      @Override
      protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) {
          int i = (this.width - this.xSize) / 2;
          int j = (this.height - this.ySize) / 2;
          this.drawDefaultBackground();
          this.mc.getTextureManager().bindTexture(background);
          this.drawTexturedModalRect(i, j, 0, 0, this.xSize, this.ySize);
      
          int timePassed = this.tile.getField(1);
          int textureWidth = (int) (23f / 200f * timePassed);
          this.drawTexturedModalRect(i + 81, j + 24, 177, 18, textureWidth, 7);
      
          if (this.tile.isBurning()) {
              int burningTime = this.tile.getField(0);
              int textureHeight = (int) (12f / this.tile.getFullBurnTime() * burningTime);
              this.drawTexturedModalRect(i + 37, j + 26 + 12 - textureHeight,
                      177, 12 - textureHeight, 27, textureHeight);
          }
      
          this.fontRenderer.drawString(this.tile.getName(), i + 80, j + 45, 0xFFFFFF);
      }
      

      Avec cette texture :
      0_1529702355595_custom_furnace.png

      Les valeurs ont été ajustées en debug, le petit scarabé à côté du bonton play, en debug mode vous pouvez changer les
      valeurs dans les fonctions et ce sera automatiquement mis à jour en jeu. (Pour le container il fait ré-ouvrir le GUI)

      Il vous reste plus qu’à mettre le GuiHandler comme il le faut, aller je vous le donne :

      public class GuiHandler implements IGuiHandler {
      
          @Override
          public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
              TileEntity te = world.getTileEntity(new BlockPos(x, y, z));
              if(te instanceof TileCustomFurnace) {
                  return new ContainerCustomFurnace((TileCustomFurnace)te, player.inventory);
              }
              return null;
          }
      
          @Override
          public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
              TileEntity te = world.getTileEntity(new BlockPos(x, y, z));
              if(te instanceof TileCustomFurnace) {
                  return new GuiCustomFurnace((TileCustomFurnace)te, player.inventory);
              }
              return null;
          }
      }
      

      Résultat

      0_1529702367263_custom_furnace_final.png

      Crédits

      Rédaction :

      • BrokenSwing

      Correction :

      • BrokenSwing

      Creative Commons
      Ce tutoriel de 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

      posted in Les interfaces (GUI) et les container
      BrokenSwing
      BrokenSwing
    • RE: Block qui prend la texture d'un autre bloc(piège).

      Je te met ça ici : https://www.minecraftforgefrance.fr/showthread.php?tid=2863

      posted in 1.7.x
      BrokenSwing
      BrokenSwing
    • RE: [1.12] Créer un four

      Dans le constructeur de ton container tu as oublié

      this.tile = tile;
      
      posted in Les interfaces (GUI) et les container
      BrokenSwing
      BrokenSwing

    Latest posts made by BrokenSwing

    • RE: Problème de rendue d'armure et des items

      Bonjour,

      Pourrait-tu envoyer l’arborescence de tes fichiers ainsi que le contenu du fichier modèle

      posted in Support pour les moddeurs
      BrokenSwing
      BrokenSwing
    • RE: build.gradle

      C’est dans le cas où tu veux proposer une modification au code de Forge directement. Cela t’explique comment t’y prendre

      posted in 1.12.x
      BrokenSwing
      BrokenSwing
    • RE: Les capabilities

      @SangloZ Salut,
      Ce genre de passage est justement pour que vous ne vous contentiez pas simplement de copier/coller le tutoriel et que vous fassiez l’effort d’apprendre le Java.

      Je ne le montre pas ici mais dans le constructeur de la classe j’initialise ce champ avec l’implémentation par défaut de notre capability.

      Je ne fais qu’une seule chose dans ce constructeur : initialiser la propriété pollution en lui donnant comme valeur l’implémentation par défaut de la capability. Celle-ci peut-être appelée directement depuis le constructeur new DefaultPollution() ou en passant par la capability PollutionStorage.POLLUTION_CAPABILITY.getDefaultInstance().

      posted in Autres
      BrokenSwing
      BrokenSwing
    • RE: [1.12] Créer un four

      @Aiko C’est à dire que tu n’as pas bien suivi le tuto 🙂 Il te manque très certainement la ligne suivante : GameRegistry.registerTileEntity(TileCustomFurnace.class, "modid:tile_custom_furnace");

      posted in Les interfaces (GUI) et les container
      BrokenSwing
      BrokenSwing
    • RE: [1.12] Créer un four

      Salut,

      Tu n’as pas enregistré ta TileEntity dans le registre.

      posted in Les interfaces (GUI) et les container
      BrokenSwing
      BrokenSwing
    • RE: Créer un bloc type four (machine)

      @RotorTV Il faut jouer avec la valeur contenue dans la variable workingTimeNeeded dans la classe de la TileEntity. Tu n’aura pas plus d’informations, le support de la version 1.7.10 n’étant plus d’actualité sur ce forum.

      posted in Les interfaces (GUI) et les container
      BrokenSwing
      BrokenSwing
    • RE: [1.12] Créer un four

      @Gollum1er C’est réglé, merci

      posted in Les interfaces (GUI) et les container
      BrokenSwing
      BrokenSwing
    • RE: un item qui pose un bloc

      Salut,
      Ce que tu cherches à faire existe déjà dans Minecraft. En effet, tous les “blocs” que tu tiens dans ta main sont en réalité des objets qui ont comme comportement de placer dans le monde le bloc leur correspondant.
      Ainsi, une classe possédant ce comportement existe déjà, elle s’appelle ItemBlock. Tu dois certainement être capable de faire ce que tu veux en utilisant cette classe. Si tu veux que l’item qui pose le bloc ai une texture et un modèle différent que celui du bloc qu’il pose alors c’est totalement possible sans modifier la classe ItemBlock.

      N’hésite pas à demander comment faire certaines choses avant de copier/coller la classe ItemBlock pour faire ce tu souhaites.

      posted in 1.13.x
      BrokenSwing
      BrokenSwing
    • RE: Les fichiers de texture et modèles JSON

      @TheXrayFR Soucis réglé

      posted in Les blocs
      BrokenSwing
      BrokenSwing
    • RE: Demande de tutoriel

      1.7.10 ? lul

      posted in Le salon libre
      BrokenSwing
      BrokenSwing
    Design by Woryk
    Contact / Mentions Légales / Faire un don

    MINECRAFT FORGE FRANCE © 2018

    Powered by NodeBB