Zero CORE multiblock API tutorial

Zero CORE ship with a revised version of Erogenous Beef Multiblock API, the base on witch is mod, Big Reactors, was built upon. While I was working on porting the API to Minecraft 1.8, 1.9.4 and now 1.10.2, I took the liberty to modify it a bit, to adapt it to the newer Minecraft versions and make it work smoothly with multiple client mods using it at the same time

This is a tutorial on how to use the API to build multiblock machines in your own mod, in the same way I’m using it in my port of Big Reactors to Minecraft 1.9.4 and 1.10.2

The tutorial is based on a test multiblock machine I made while I was testing my version of the API. The full source code of my test code is available on my GitHub account at https://github.com/ZeroNoRyouki/ZeroTest

In this tutorial you will learn:

 

How to add Zero CORE to your own project

Adding Zero CORE to your project is pretty easy. The only real requirements is that your project is a Minecraft Forge mod for Minecraft 1.9.4 or 1.10.2 (or newer versions)

Open your build.gradle file in your IDE or a simple text editor and look for a “repositories” section OUTSIDE the “buildscript” section. If there is one, add a reference to my maven repository inside it:

maven {
    name "zerocore" url "http://maven.zerono.it/"
}

If you don’t have a “repositories” section OUTSIDE the “buildscript” section, add it like this after the “minecraft” section:

repositories {
    maven {
        name "zerocore" url "http://maven.zerono.it/"
    }
}

With this step we are basically telling ForgeGradle where to look for Zero CORE

Now, look for a “dependencies” section OUTSIDE the “buildscript”. If you have one, add the following line inside it:

compile group: "it.zerono.mods.zercore", name: "zerocore", version: "1.10.2-0.0.4"

If you don’t have a “dependencies” section OUTSIDE the “buildscript”, add one after the “repositories” section you used or created in the previous steps:

dependencies {

    compile group: "it.zerono.mods.zerocore", name: "zerocore", version: "1.10.2-0.0.4"

}

If you want a different version of Zero CORE just change the version in the “compile” directive. A full list of the available version can be found on my Maven repository at http://maven.zerono.it

Save the build.gradle file and have your IDE reload it

What is a multiblock machine and how the multiblock API works

A multiblock machine is a machine made up from “parts”: multiple blocks (you guessed that right?) that once completed, act like a single bigger machine and not just like a bunch of blocks put together

To make a multiblock machine with this API you need three components:

  • the blocks themselves to build the machine. You can have only one type of block or different types, it’s totally up to you and what your mod/machine is about

  • the blocks tile entities. Each block of the machine must create a special-made, NON ticking, tile entity that it is used to implement the functionality of the single block type (if they have one) and to link the block together as a single machine

  • the multiblock controller. This is a special class that it is in charge of the whole machine. You can truly say that the controller IS the machine: it dictate how the machine is considered complete, what can be part of the machine or not (a duty shared with the tile entities) and, most importantly, what the machine actually do

The multiblock API expose to it’s users a base class for a multiblock tile entity (MultiblockTileEntityBase) and one for a controller (MultiblockControllerBase). What you have to do is create your own tile entities and controllers by extending those base classes and add blocks for those tile entities

Under the hood, each multiblock part and controllers is tracked by a multiblock registry that will coordinate the creation or destruction of the whole machine, raising various events on the controllers or tile entities themselves

A multiblock machine can be made in any shape you like, you just need a controller class and a tile entity class that validates your chosen shape. The API ship with ready to run classes for a rectangular machine (RectangularMultiblockTileEntityBase and RectangularMultiblockControllerBase)

In our example mod we will create a 3x3x3 multiblock “furnace”, made up by a power input port to give the machine power, an inventory input port to supply items to smelt, an output port to extract the smelted product and a “wall” block that make the bulk of the machine

Please be advised that I will only cover the multiblock aspect of the machine, not how to make the furnace itself work

The multiblock controller

The controller is the brain of the multiblock machine. It dictate what can be used in a multiblock machine, what are the shape and dimension of the machine and what the machine actually do once assembled

You create you own controller by extending MultiblockControllerBase. If you machine would be rectangular in shape, just extend RectangularMultiblockControllerBase

– Machine parts and validation

One of the duty of the controller is to validate the machine while it is begin built by the player

There is a number of methods (abstract or not) that you need to implement or override to describe the basic shape of the machine to the API:

protected abstract int getMinimumNumberOfBlocksForAssembledMachine();

You must return the minimum number of blocks needed for it to be considered assembled

protected abstract int getMaximumXSize();

protected abstract int getMaximumZSize();

protected abstract int getMaximumYSize();

The maximum size of the machine in the given direction

protected int getMinimumXSize()

protected int getMinimumYSize()

protected int getMinimumZSize()

The minimum size of the machine in the given direction. They return 1 by default

protected abstract boolean isMachineWhole(IMultiblockValidator validatorCallback)

This is where you check if your machine is built correctly with your own logic. You should return true if the machine is built correctly. If there are any problems, you must use validatorCallback to set an error for the player (you can retrieve it using getLastError()) before returning false.

If you extend RectangularMultiblockControllerBase and override this method, make sure to call it on the super class BEFORE executing your own logic

All controllers keep track of their associated parts: you can access them using the connectedParts field of the controller. When a part is added or removed by the player, the controller is notified trough the onBlockAdded() and onBlockRemoved() methods

When the machine assembly state change, the following methods are called:

onMachineAssembled() : a previously disassembled machine is now assembled

onMachineDisassembled() : a previously assembled machine is now disassembled (eg: a block was broken by a player)

onMachinePaused() : a previously assembled machine is now paused. A machine is paused for non-player related issues (eg: a chunk is unloaded)

onMachineRestored() : a previously paused machine is now assembled

– Validation of blocks without a tile entity

You can use vanilla blocks or, in general, blocks without a MultiblockTileEntityBase-derived tile entity, as components for your machine. To validate this blocks, the following methods should be implemented in your controller class. RectangularMultiblockControllerBase automatically call them when such a block is found inside the volume of the machine

protected abstract boolean isBlockGoodForFrame(World world, int x, int y, int z, IMultiblockValidator validatorCallback);

Check if the block at the given location is valid for the machine frame (the external perimeter of the machine)

protected abstract boolean isBlockGoodForTop(World world, int x, int y, int z, IMultiblockValidator validatorCallback);

Check if the block at the given location is valid for the machine top face

protected abstract boolean isBlockGoodForBottom(World world, int x, int y, int z, IMultiblockValidator validatorCallback);

Check if the block at the given location is valid for the machine bottom face

protected abstract boolean isBlockGoodForSides(World world, int x, int y, int z, IMultiblockValidator validatorCallback);

Check if the block at the given location is valid for the sides of the machine

protected abstract boolean isBlockGoodForInterior(World world, int x, int y, int z, IMultiblockValidator validatorCallback);

Check if the block at the given location is valid for the interior space of the machine

For all of them, you should return true if the block is valid or set an error and return false

– Saving and loading back the controller/machine state

An important concept is the controller save delegate: one of the parts attached to the controller will be given the responsibility of saving and loading the internal data of the controller (and the machine) along side it owns (if it has any). This is all handled by the base classes, you just need to implement the methods syncDataFrom() to load data from a NBTTagCompound and syncDataTo() to save the controller data. We will talk more of this two methods in the section about tile entities

– Controller ticking

To do some actual work you have to implement the following method:

protected abstract boolean updateServer();

If the machine is assembled, this method is called, on the server thread, every game tick, so try to keep this code small and fast. Return true if the internal state of the controller was updated (the API will mark all the chunks in witch a block of the machine is present as dirty so they will be saved on disk)

If you need to run some code on the client, override the following method:

protected abstract void updateClient();

The multiblock tile entity

Each tile entities of your multiblock machine much be derived from MultiblockTileEntityBase. For a rectangular shaped machine you can use RectangularMultiblockTileEntityBase instead

– Part validation

The tile entity will be asked if it is valid for the position it is currently in when the validation check is performed. One of the isGoodFor**** methods will be called depending on the position of the tile entity. The default implementation simply return true.

– Controller validation and creation

You must implement the following methods to let the API known the correct controller for your tile entities

Class<? extends MultiblockControllerBase> getMultiblockControllerType(): you must return the Class of your controller

MultiblockControllerBase createNewMultiblock(): when called, create a new instance of your controller

If you need to access the controller instance attached to your tile entity you can call isConnected() to check if the tile entity is attached to a controller and then getMultiblockController() to retrieve the controller

– Saving and loading back the tile entity state

MultiblockTileEntityBase extend Zero CORE own tile entity utility class (ModTileEntity) to take advantage of it’s unified state synchronization mechanism: syncDataTo() and syncDataFrom() are implemented to save data and load it back when Minecraft or the multiblock API ask for it

Both methods receive a NBTTagCompound for saving/loading the tile entity state and a SyncReason enumeration that describe the sync operation requested.

A FullSync request is asking for the complete state of the tile entity to be saved or loaded. This usually happen when the tile entity is first loaded from disk on the server side

A NetworkUpdate occur when the state of the tile entity is needed to update the other side using network packets or messages. If only a portion of the tile entity state is needed (or changed) to update the other side you could just send that, avoiding a full update

– The save delegate

One of the machine tile entities will be charged with the duty of saving the machine state as well as it’s own. That tile entity will become the save delegate for the whole machine. This is all handled internally by the multiblock API but if need it, you can override becomeMultiblockSaveDelegate() and forfeitMultiblockSaveDelegate() to be notified when a tile entity is elected a the save delegate or when it loose the position

– Tile entities and rectangular machines

RectangularMultiblockTileEntityBase expose the position of the tile entity in the whole machine and it’s “outward facing”, that is, the faces of part block that are exposed to the outside world

getPartPosition() return where the part is in the machine (interior, frame, corners, a face, etc)

getOutwardsDir() return an BlockFacings, a Zero CORE general purpose class to track the state of all 6 faces of a block. If a face of the part block is exposed to the outside of the machine, it’s corresponding EnumFacing value inside the BlockFacings will be set to true. This information is particularly useful to creating the correct blockstate for rendering your blocks

Blocks of the multiblock machine

The actual blocks of the machine are normal blocks that create your tile entities when placed. Usually their blockstate is composed using data retrieved from the block tile entity and/or the machine controller

The Mighty Furnace

Our example machine is a 3x3x3 multiblock composed by the following parts:

  • a base/wall block to build the bulk of the machine
  • a power-port block to simulate a block that accept energy for the machine (only 1 per machine)
  • an input-port block to simulate a block that accept items to smelt in the furnace (only 1 per machine)
  • an output-port block to simulate a block that will output the smelted items (only 1 per machine)

Each of the 3 ports must be placed on one of the side-faces of the machine. The top or bottom faces are off limits

From the point of view of the multiblock library you will need a controller class and a tile entity class for all your blocks. You can implement this class any way you like. I’ve choosen to go with the following design:

  • MightyFurnaceController : the controller for the machine. As the furnace is going to be a 3x3x3 cube, I’ll create the controller by extending RectangularMultiblockControllerBase to take advantage of the built in support for rectangular machines
  • MightyFurnaceTileEntity : base class for the machine tile entities created extending RectangularMultiblockTileEntityBase. This will be the tile entity for the wall blocks
  • MightyFurnacePowerTileEntity : the tile entity for the power-port and a sub class of MightyFurnaceTileEntity
  • MightyFurnaceIOPortTileEntity : the tile entity for the input and output port, also a sub class of MightyFurnaceTileEntity
  • MightyFurnaceBlockBase : base class for the blocks themself
  • MightyFurnaceBlockWall : the wall block, a subclass of MightyFurnaceBlockBase
  • MightyFurnaceBlockPort : the block for the input, output and power port, also a subclass of MightyFurnaceBlockBase

I will only highlight the most relevant code of the classes here. You can find their full implementation at https://github.com/ZeroNoRyouki/ZeroTest

In our simulation, we will have the player sneak-click the furnace to activate it (or deactivate it if it was already active).  A simple click will give him the last validation error if any is available

 

MightyFurnaceController

Let’s start with what kind of internal state our controller should keep. As it would probably work closely with the ports tile entity, we will keep a reference of each one of them. Then, to simulate the active/inactive state of the furnace, we will add a simple boolean field.

private MightyFurnaceIOPortTileEntity _inputPort;
private MightyFurnaceIOPortTileEntity _outputPort;
private MightyFurnacePowerTileEntity _powerPort;
private boolean _active;

In the constructor we set all the references to null and the boolean to false

The only blocks allowed in our machines are the one we are creating for it, so any other blocks found in the volume of our machine is invalid. All the isBlockGoodFor*** methods simply discard any blocks they are asked to valutate:

@Override
protected boolean isBlockGoodForFrame(World world, int x, int y, int z, IMultiblockValidator validatorCallback) {

    validatorCallback.setLastError("Zero COREtest:api.multiblock.validation.invalid_block", x, y, z);
    return false;
}

We simply set a validation error message using the IMultiblockValidator  and return false. Please note that the error message is a translation key, pointing to the actual message defined in the language file:

zerotest:api.multiblock.validation.invalid_block=Block at %1$d, %2$d, %3$d is not valid for the machine

This will allow you to provide a translation for the validation messages in all the languages supported by your mod

Our machine is going to be a 3x3x3 cube with a fixed blocks count of 27 blocks and we tell this to the API by overriding the following methods:

@Override
protected int getMinimumNumberOfBlocksForAssembledMachine() {return 27;}

@Override
protected int getMaximumXSize() {return MACHINE_SIZE;}

@Override
protected int getMaximumZSize() {return MACHINE_SIZE;}

@Override
protected int getMaximumYSize() {return MACHINE_SIZE;}

@Override
protected int getMinimumXSize() {return MACHINE_SIZE;}

@Override
protected int getMinimumYSize() {return MACHINE_SIZE;}

@Override
protected int getMinimumZSize() {return MACHINE_SIZE;}

private static final int MACHINE_SIZE = 3;

 

To validate our machine, we need to override isMachineWhole() :

@Override
protected boolean isMachineWhole(IMultiblockValidator validatorCallback) {

    MightyFurnacePowerTileEntity powerPort = null;
    MightyFurnaceIOPortTileEntity inputPort = null;
    MightyFurnaceIOPortTileEntity outputPort = null;

    if (!super.isMachineWhole(validatorCallback))
        return false;

In this method we will check all the blocks found in the machine, looking for our ports (exactly one of each of them). Before we do that, we must call the isMachineWhole on the base class and bail out if that check fail

To check all the blocks we iterate on connectedParts. Thanks to the isBlockGoodFor*** methods we defined early, we known that the only tile entities we can find here are our owns.

    for (IMultiblockPart part : this.connectedParts) {

        if (part instanceof MightyFurnacePowerTileEntity) {

            if (null != powerPort) {

                validatorCallback.setLastError("Zero COREtest:api.multiblock.validation.powerport_already_present");
                return false;
            }

            powerPort = (MightyFurnacePowerTileEntity)part;

If the tile entity is a MightyFurnacePowerTileEntity, we check if we already found one and give an error in that case. The same kind of check is done for the other two ports

        } else if (part instanceof MightyFurnaceIOPortTileEntity) {

            MightyFurnaceIOPortTileEntity io = (MightyFurnaceIOPortTileEntity) part;
            boolean isInput = io.isInput();

            if (isInput) {

                if (null != inputPort) {

                    validatorCallback.setLastError("Zero COREtest:api.multiblock.validation.inputport_already_present");
                    return false;
                }

                inputPort = io;

            } else {

                if (null != outputPort) {

                    validatorCallback.setLastError("Zero COREtest:api.multiblock.validation.outputport_already_present");
                    return false;
                }

                outputPort = io;
            }
        }
    }

After we scanned all the tile entities in the machine, we set a validation error if any of the three type of tile entity is missing

    if (null == powerPort) {

        validatorCallback.setLastError("Zero COREtest:api.multiblock.validation.powerport_missing");
        return false;
    }

    if (null == inputPort) {

        validatorCallback.setLastError("Zero COREtest:api.multiblock.validation.inputport_missing");
        return false;
    }

    if (null == outputPort) {

        validatorCallback.setLastError("Zero COREtest:api.multiblock.validation.outputport_missing");
        return false;
    }

    return true;
}

Please note that we are only checking if the ports tile entities are there. We are not yet storing a reference to them in your internal fields, waiting for then the machine is assembled (or restored):

@Override
protected void onMachineAssembled() {

    this.lookupPorts();

    if (WorldHelper.calledByLogicalClient(this.WORLD))
        // on the client, force a render update
        this.markMultiblockForRenderUpdate();
}

A note on Zero CORE 1.10.2-0.1.0.1+

 

From version 1.10.2-0.1.0.1, the onMachineAssembled() and onMachineBroken() methods are deprecated.

 

New methods were added to replaced them:

  • onPreMachineAssembled
  • onPreMachineBroken
  • onPostMachineAssembled
  • onPostMachineBroken

The new methods are called immediately before (Pre) a machine is assembled/broken and immediately after (Post) a machine is assembled/broken.

 

In most cases, what you did in onMachineAssembled/onMachineBroken should be moved to the equivalents Post methods.

The lookupPorts() method just scan again the tile entities found in the machine and save a reference to the three kind of ports in the controller internal data. We also ask the client to re-render the whole multiblock as we have a different texture for our blocks when they are assembled together

If one of the three ports is removed, we need to null-out our internal reference to make sure we don’t work with a removed tile entity (and also to not keep the tile entity from getting collected). To do that, we override onBlockRemoved() :

@Override
protected void onBlockRemoved(IMultiblockPart oldPart) {

    if (oldPart instanceof MightyFurnacePowerTileEntity) {

        MightyFurnacePowerTileEntity tile = (MightyFurnacePowerTileEntity)oldPart;

        if (this._powerPort == tile)
            this._powerPort = null;

    } else if (oldPart instanceof MightyFurnaceIOPortTileEntity) {

        MightyFurnaceIOPortTileEntity tile = (MightyFurnaceIOPortTileEntity)oldPart;

        if (this._outputPort == tile)
            this._outputPort = null;
        else if (this._inputPort == tile)
            this._inputPort = null;
    }
}

Lastly, to simulate the machine activation we provide a public method to activate or deactivate the machine:

public void setActive(boolean active) {

    if (this._active == active)
        return;

    // the state was changed, set it
    this._active = active;

After the state was updated (if the machine was not in that state already), send out an update to the connected clients if we were called on the server thread. On the client, just request a render update

    if (WorldHelper.calledByLogicalServer(this.WORLD)) {

        // on the server side, request an update to be sent to the client and mark the save delegate as dirty
        this.markReferenceCoordForUpdate();
        this.markReferenceCoordDirty();

    } else {

        // on the client, request a render update
        this.markMultiblockForRenderUpdate();
    }
}

public void toggleActive() {
    this.setActive(!this._active);
}

The toggleActive() method is just an utility method to invert the state of the machine

As you may recall, our tile entities are synchronized using the syncDataFrom() and syncDataTo() methods from ModTileEntity :

@Override
protected void syncDataTo(NBTTagCompound data, ModTileEntity.SyncReason syncReason) {
    data.setBoolean("isActive", this.isActive());

Here we are just saving the current state in the NBTTagCompound
}

@Override
protected void syncDataFrom(NBTTagCompound data, ModTileEntity.SyncReason syncReason) {

    if (data.hasKey("isActive"))
        this.setActive(data.getBoolean("isActive"));

Please note that we call setActive() to set the state of the machine to the value loaded from the NBTTagCompound : this will ensure that the correct behavior is carried out if we are on the server side of the game (update the state and notify all the clients) or the client side (request a render update)
}

MightyFurnaceTileEntity

The base tile entity class is pretty simple. We just need to approve any validation request as a “wall” part can be placed anywhere in the machine so all the isGoodFor**** methods just return true

Probably the most important job of this class is providing informations on the correct controller to use for our tile entities:

@Override
public Class<? extends MultiblockControllerBase> getMultiblockControllerType() {
    return MightyFurnaceController.class;
}

@Override
public MultiblockControllerBase createNewMultiblock() {
    return new MightyFurnaceController(this.worldObj);
}
MightyFurnacePowerTileEntity and MightyFurnaceIOPortTileEntity

As this is just an example multiblock, this two tile entities do almost nothing except stopping the validation process if they are not placed on the side of the machine. They do that by overriding isGoodForFrame(), isGoodForTop(), isGoodForBottom() and isGoodForInterior() and setting a validation error in them:

@Override
public boolean isGoodForFrame(IMultiblockValidator validatorCallback) {

    validatorCallback.setLastError(MightyFurnaceIOPortTileEntity.s_invalidPosition);
    return false;
}

private static ValidationError s_invalidPosition = new     ValidationError("Zero COREtest:api.multiblock.validation.ioport_invalid_position");

As the validation error is always the same in all four methods, we cache an instance of the error in a static field and just return it to avoid creating the same object every time one of those methods are called

 

MightyFurnaceBlockBase

This is a pretty standard block class and there is not much going on here except creating the correct tile entity and responding to the player clicking on our blocks: if the player click on one of the blocks while sneaking we will invert the state of the machine. If he click without sneaking we will retrieve the last validation error from the machine controller and display it to the player

protected MightyFurnaceController getFurnaceController(IBlockAccess world, BlockPos position) {

    MultiblockControllerBase controller = this.getMultiblockController(world, position);

    return controller instanceof MightyFurnaceController ? (MightyFurnaceController)controller : null;
}

The above method is just an utility method to retrieve a correctly typed controller instance from our tile entity

@Override
public boolean onBlockActivated(World world, BlockPos position, IBlockState state, EntityPlayer player, EnumHand hand, ItemStack heldItem, EnumFacing side, float hitX, float hitY, float hitZ) {

    if (world.isRemote || (hand != EnumHand.OFF_HAND) || (null != heldItem))
        return false;

If we are on the client thread, the player is using the off hand or it’s holding something we bail out and do noghing

    MightyFurnaceController controller = this.getFurnaceController(world, position);

    if (null != controller) {

        if (player.isSneaking()) {

            // toggle machine status
            controller.toggleActive();
            return true;

This is pretty straight forward: if the player is sneaking we invert the machine state

        } else {

            // display any validation errors

            ValidationError status = controller.getLastError();

            if (null != status) {

                player.addChatMessage(status.getChatMessage());
                return true;
            }

We first ask the controller if there is a validation error laying around. If we got one, we add the message (with the correct translation in place) to the player chat
        }
    }

    return false;
}

Lastly, let’s add an utility method that wrap World.getTileEntity() to easily retrieve a multiblock part from the world:

protected IMultiblockPart getMultiblockPartAt(IBlockAccess world, BlockPos position) {

    TileEntity te = world.getTileEntity(position);

    return te instanceof IMultiblockPart ? (IMultiblockPart)te : null;
}

 

MightyFurnaceBlockPort

It’s interesting to look at MightyFurnaceBlockPort to see how we could use information retried from the block tile entity or the machine controller to build the correct blockstate and render our block differently if the machine is assembled or not: we do that by overriding the vanilla method getActualState(). The blockstate of the port contain two properties:

  • ASSEMBLED : a boolean property that it’s true when the machine is assembled
  • HFACING : one of the horizontal EnumFacing values to indicate the orientation of the port

The getActualState() method is defined as:

@Override
public IBlockState getActualState(IBlockState state, IBlockAccess world, BlockPos position) {

    IMultiblockPart part = this.getMultiblockPartAt(world, position);

    if (part instanceof MightyFurnaceTileEntity) {

        MightyFurnaceTileEntity wallTile = (MightyFurnaceTileEntity)part;
        boolean assembled = wallTile.isConnected() && wallTile.getMultiblockController().isAssembled();

        state = state.withProperty(ASSEMBLED, assembled);

We begin by first retrieving the multiblock part the the block position and, if it’s one of our tile entities, we ask the controller (if there is one) if the machine is assembled or not and then we add this information to the blockstate

Then, if the machine is assembled, we check where our tile entity is in the machine by calling getPartPosition() and set the HFACING property as appropriate

        if (assembled) {

            switch (wallTile.getPartPosition()) {

                case NorthFace:
                    state = state.withProperty(HFACING, EnumFacing.NORTH);
                    break;

                case SouthFace:
                    state = state.withProperty(HFACING, EnumFacing.SOUTH);
                    break;

                case WestFace:
                    state = state.withProperty(HFACING, EnumFacing.WEST);
                    break;

                case EastFace:
                    state = state.withProperty(HFACING, EnumFacing.EAST);
                    break;
            }
        }
    }

    return state;
}

This is the blockstate JSON file for the power port:

    {
        "forge_marker": 1,
        "defaults": {
            "textures": {
                "side": "zerotest:blocks/multiblock/mightyfurnace/SingleBlockSimple",
                "particle": "#side",
                "down": "#side",
                "up": "#side",
                "north": "#side",
                "south": "#side",
                "west": "#side",
                "east": "#side"
            },
            "model": "cube"
        },
        "variants": {
            "assembled": {
                "true": {"textures": {"facing": "zerotest:blocks/multiblock/mightyfurnace/FaceCenterPower" }},
                "false": {"textures": {"facing": "zerotest:blocks/multiblock/mightyfurnace/SingleBlockPowerInput" }}
            },
            "hfacing": {
                "north": {"textures": {"north": "#facing" }},
                "south": {"textures": {"south": "#facing" }},
                "west": {"textures": {"west": "#facing" }},
                "east": {"textures": {"east": "#facing" }}
            },
            "inventory": {
                "transform": "forge:default-block",
                "textures": {
                    "north": "zerotest:blocks/multiblock/mightyfurnace/SingleBlockPowerInput"
                }
            }
        }
    }

 

Conclusions

We are come to the end of this tutorial. I hope this would be helpful to other modders and that we would see more multiblock machine in Minecraft! 🙂

Please let me know if something is not clear enough in this tutorial or if you have any question about the multiblock API or Zero CORE. I can be reached on twitter looking for @ZeroNoRyouki