1 mars 2017, 14:23

Sommaire

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

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