API tutorials: creating a network node

This tutorial applies to Refined Storage version 1.5.x, on Minecraft 1.12.

What is a network node?

A network node is a tile that can be connected to a storage network. Depending on implementation, it can also act as a conductor in the network graph to pass a network signal (like cables).

Tile entity vs INetworkNode

Network nodes are represented as a class that implements INetworkNode. Note that an actual network node isn't stored in the tile entity. Every network node is decoupled from the tile entity. All network nodes are offloaded to a manager that is world-dependant.

This is very important, as it prevents a lot of bugs regarding chunkloading and unloading.

Implementation

Getting the Refined Storage API

We'll need some helper methods from the Refined Storage API first.

import com.raoulvdberge.refinedstorage.api.IRSAPI;
import com.raoulvdberge.refinedstorage.api.RSAPIInject;

public class RSHelper {
    @RSAPIInject
    public static IRSAPI API;
}

Implementing the INetworkNode

Here is an example implementation of a INetworkNode:

import com.raoulvdberge.refinedstorage.api.network.INetwork;
import com.raoulvdberge.refinedstorage.api.network.INetworkNeighborhoodAware;
import com.raoulvdberge.refinedstorage.api.network.node.INetworkNode;
import com.raoulvdberge.refinedstorage.apiimpl.API;
import net.minecraft.block.state.IBlockState;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class NetworkNode implements INetworkNode {
    @Nullable
    protected INetwork network;
    protected World world;
    protected BlockPos pos;

    public NetworkNode(World world, BlockPos pos) {
        this.world = world;
        this.pos = pos;
    }

    @Override
    public int getEnergyUsage() {
        return 0;
    }

    @Nonnull
    @Override
    public ItemStack getItemStack() {
        IBlockState state = world.getBlockState(pos);

        return new ItemStack(Item.getItemFromBlock(state.getBlock()), 1, state.getBlock().getMetaFromState(state));
    }

    @Override
    public void onConnected(INetwork network) {
        this.network = network;
    }

    @Override
    public void onDisconnected(INetwork network) {
        this.network = null;
    }

    @Override
    public boolean canUpdate() {
        return true;
    }

    @Nullable
    @Override
    public INetwork getNetwork() {
        return network;
    }

    @Override
    public void update() {

    }

    @Override
    public NBTTagCompound write(NBTTagCompound tag) {
        return tag;
    }

    @Override
    public BlockPos getPos() {
        return pos;
    }

    @Override
    public World getWorld() {
        return world;
    }

    @Override
    public void markDirty() {
        if (!world.isRemote) {
            RSHelper.API.getNetworkNodeManager(world).markForSaving();
        }
    }

    @Override
    public String getId() {
        return "id_here";
    }

    @Override
    public boolean equals(Object o) {
        return RSHelper.API.isNetworkNodeEqual(this, o);
    }

    @Override
    public int hashCode() {
        return RSHelper.API.getNetworkNodeHashCode(this);
    }
}

Most of it is self-explanatory. Some additional notes:

  • We are using id_here as the network node id, we're going to be using this later as well
  • The update method needs to contain the logic of your network node, it is only called on the server
  • The update method will be called even if the network node isn't connected to any network
  • Implementing equals and hashCode is mandatory! The network graph uses this for fast network scanning

Registering the network node

As you can see our example INetworkNode implementation doesn't have any read(NBTTagCompound tag) method. We'll implement this now, by registering our network node.

We need to register the network node so Refined Storage can deserialize it when the world loads.

In your preInit event handler, put this:

RSHelper.API.getNetworkNodeRegistry().add("id_here", (tag, world, pos) -> {
    NetworkNode node = new NetworkNode(world, pos);

    // do something with tag

    return node;
});

Note that you should use the id that INetworkNode#getId returns. We are using id_here like in our example INetworkNode implementation above.

Implementing our tile entity

As I said before the network node is decoupled from the tile entity by using the INetworkNode class. This is very important, as it prevents a lot of bugs regarding chunkloading and unloading.

It's important that you respect this, and leave network node logic in the INetworkNode class.

import com.raoulvdberge.refinedstorage.api.network.node.INetworkNodeManager;
import com.raoulvdberge.refinedstorage.api.network.node.INetworkNodeProxy;
import com.raoulvdberge.refinedstorage.api.network.node.NetworkNode;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraftforge.common.capabilities.Capability;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class TileNode extends TileEntity implements INetworkNodeProxy<NetworkNode> {
    @CapabilityInject(INetworkNodeProxy.class)
    private static final Capability<INetworkNodeProxy> NETWORK_NODE_PROXY_CAPABILITY = null;

    private NetworkNode clientSideNode;

    @Nonnull
    @Override
    public NetworkNode getNode() {
        if (world.isRemote) {
            if (clientSideNode == null) {
                clientSideNode = new NetworkNode();
            }

            return clientSideNode;
        }

        INetworkNodeManager manager = RSHelper.API.getNetworkNodeManager(world);

        NetworkNode node = (NetworkNode) manager.getNode(pos);

        if (node == null || !node.getId().equals("id_here")) {
            manager.setNode(pos, node = new NetworkNode(world, pos));
            manager.markForSaving();
        }

        return node;
    }

    @Override
    public boolean hasCapability(@Nonnull Capability<?> capability, @Nullable EnumFacing side) {
        if (capability == NETWORK_NODE_PROXY_CAPABILITY) {
            return true;
        }

        return super.hasCapability(capability, side);
    }

    @Override
    public <T> T getCapability(@Nonnull Capability<T> capability, @Nullable EnumFacing side) {
        if (capability == NETWORK_NODE_PROXY_CAPABILITY) {
            return NETWORK_NODE_PROXY_CAPABILITY.cast(this);
        }

        return super.getCapability(capability, side);
    }
}

As you see we make the tile entity implement the INetworkNodeProxy interface, which is a capability. In the getNode method we return the actual network node. The tile entity merely acts as a proxy.

Before we attempt to get the network node out of the INetworkNodeManager, we first check if we're on the client and return a "dummy" node instead. It's important to cache this node to maintain state. The client has no such concept of a INetworkNodeManager, and you'll crash if you try to retrieve it on the client.

Next, we retrieve the INetworkNodeManager for the world and try to get the node for our current position. If there is no such node on the current position, we create it and put it in the manager. Important is that you mark the manager dirty as well with INetworkNodeManager#markForSaving. We also re-create the node if the id of the node on disk doesn't match up with the tile entities' node (this happens with rollback commands that rollback chunks but not the Refined Storage node data file).

Differences between INetworkNodeManager and INetworkNodeRegistry

The INetworkNodeManager stores network nodes for a world.

The INetworkNodeRegistry stores factories for deserializing network nodes from disk.

Implementing our block

Removal of network nodes

The block needs some code to remove the network node from the INetworkNodeManager when broken:

@Override
public void breakBlock(World world, BlockPos pos, IBlockState state) {
    super.breakBlock(world, pos, state);

    INetworkNodeManager manager = RSHelper.API.getNetworkNodeManager(world);

    INetworkNode node = manager.getNode(pos);

    manager.removeNode(pos);
    manager.markForSaving();

    if (node.getNetwork() != null) {
        node.getNetwork().getNodeGraph().rebuild();
    }
}

Important is that we call INetworkNodeGraph#rebuild, so that the network of the removed node is notified of the removal and can rebuild its graph.

Discovery of a newly placed network node

The block also needs some code to let the neighbors of the placed block know that a new network node has been placed.

@Override
public void onBlockPlacedBy(World world, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack) {
    super.onBlockPlacedBy(world, pos, state, placer, stack);

    if (!world.isRemote) {
        RSHelper.API.discoverNode(world, pos);
    }
}

This is important, so that our node will be added to the network.

Tile entity

Obviously, we also have to provide our tile entity.

@Override
public boolean hasTileEntity(IBlockState state) {
    return true;
}

@Override
public TileEntity createTileEntity(World world, IBlockState state) {
    return new TileNode();
}