Minecraft Modding: Custom Fluid Handling Items

Background


This tutorial assumes you have already created a custom Fluid. Check out Jabelar's Custom Fluid Tutorial.

An item can be a fluid handler. In fact that is what a bucket is, but you also imagine things like "bottles", "flasks", "test tubes" as interesting items to add to a mod.

The Forge fluid handling system is quite flexible, but also fairly confusing and there are several ways to accomplish the same thing. For fluid handling items, I've noticed that most people tend to make two separate registered item instances for the full and empty versions of the item (in fact this is the way the built-in UniversalBucket class does it) ; however that is unnecessary and furthermore doesn't work if you want to hold amounts in between full and empty.

Therefore, in this tutorial I explain how to make an item fluid handler that can contain a variable amount of fluid including empty with a single registered item.

Key Point: You can handle both empty and other fluid levels all within a single item instance. You should not need both an empty and full version of the instance.

A Customer Fluid Handler Using A Single Item Instance


So in order for a ItemStack contain your custom fluid, you essentially need:

  1. Create a custom fluid. See Jabelar's Custom Fluid Tutorial.
  2. Create a custom fluid handler capability class that extends FluidHandlerItemStack or FluidHandlerItemStackSimple and @Overrides methods for any behavior you wish to chance (e.g. the setContainerToEmpty() and canFillFluidType() methods are common to change).
  3. Create a custom class that extends Item and returns a new instance of your fluid handler capability from Step 2.
  4. Construct a singleton instance of your custom item from Step 3.
  5. Register your item instance from Step 4 in your item registry event handling method.
  6. Create a custom class that extends IModel along with related IBakedModelModelLoader and ItemOverrideList.
  7. Register your custom model loader, model and item variants in the model registry event handling method.
  8. Put texture assets with proper names in proper resource file locations.
Step 1. Create (And Register) A Custom Fluid

Before you can handle a fluid you need to create and register a fluid. See Jabelar's Custom Fluid Tutorial.

Step 2. Create Custom Fluid Handler Class

The built-in FluidHandlerItemStack or FluidHandlerItemStackSimple methods may suit your needs, but if you want something special to happen when the fluid handler is emptied, like destroying the container, replacing the container, or something else interesting then you need to create a custom class

In that case, I recommend simply extending one of the two built-in handlers and @Override the setContainerToEmpty() method and any other methods that make sense. For example here is one of my fluid handler classes:

public class FluidHandlerSlimeBag extends FluidHandlerItemStack
{
    // Always make a copy of this if you use it for an assignment
    protected static final FluidStack EMPTY = new FluidStack(ModFluids.SLIME, 0); 

    public FluidHandlerSlimeBag(ItemStack parContainerStack, int parCapacity) 
    {
     super(parContainerStack, parCapacity);

     // if container was constructed by loading from NBT, should already
     // have fluid information in tags
     if (getFluidStack() == null)
     {
     setContainerToEmpty(); // start empty
     }
    }

    @Override
    protected void setContainerToEmpty()
    {
        setFluidStack(EMPTY.copy()); // some code looks at level, some looks at lack of handler (tag)
        container.getTagCompound().removeTag(FLUID_NBT_KEY);
    }

    @Override
    public boolean canFillFluidType(FluidStack fluid)
    {
        return (fluid.getFluid() == ModFluids.SLIME);
    }
    
    // rename getFluid() method since it is confusing as it returns a fluid stack
    public FluidStack getFluidStack()
    {
    return getFluid();
    }
    
    // rename setFluid() method since it is confusing as it take a fluid stack
    public void setFluidStack(FluidStack parFluidStack)
    {
    setFluid(parFluidStack);
    }
}

Warning: The code in the constructor where I check for null fluid stack before setting it to a default value is extremely important. If you don't do that, it will lose the fluid level after saving and loading the game. This is due to the fact that the reading of the NBT and conversion to capability data happens before the constructor!

Step 3a.  Create A Custom Class That Extends Item And Initializes the IFluidHandler Capability

Tip: Although there is an ItemFluidContainer template class it is very simple and returns a vanilla fluid handler, as explained in Step 2 above it is preferable to use a custom fluid handler and therefore you must extend Item directly and provide the capability yourself.

Items are already capability providers so to add the IFluidHandler capability you simply need to return your fluid handler instance in the initCapabilities() method.

Furthermore, you should @Override and otherwise call setter methods to adjust all the other information you desire for the item, some of which I give in the following sub-steps.

So the constructor and initCapabilities() method can look something like this:

public class ItemSlimeBag extends Item
{
    private final int CAPACITY = Fluid.BUCKET_VOLUME;
    private final ItemStack EMPTY_STACK = new ItemStack(this);

    public ItemSlimeBag() 
    {
        setRegistryName("slime_bag");
        setUnlocalizedName("slime_bag");
        setCreativeTab(CreativeTabs.MISC);
        setMaxStackSize(1);
        BlockDispenser.DISPENSE_BEHAVIOR_REGISTRY.putObject(this, DispenseFluidContainer.getInstance());
    }

    @Override
    public ICapabilityProvider initCapabilities(@Nonnull ItemStack stack, @Nullable NBTTagCompound nbt)
    {
        return new FluidHandlerSlimeBag(stack, CAPACITY);
    }

The code above should be fairly self-explanatory. The key points are you should set up the item how you want in terms of unlocalized name, dispensing behavior, etc. as well as @Override the initCapabilities() method to return an instance of your custom fluid handler.

Warning: It is highly recommended to setMaxStackSize() to 1 because otherwise the stacking gets very complicated as you will have different fluid levels in each item and so it is not very logical to stack them.

Step 3b. In Your Custom Item Class @Override The getSubItems() And getDisplayName() Methods

Depending on how you want your item to appear in the creative tab, you likely want to @Override the getSubItems() method to provide separate instances with different fluid levels. Most commonly you will want empty and full, but you could also do things in between if desired. For example:

    @Override
    public void getSubItems(@Nullable final CreativeTabs tab, final NonNullList subItems) 
    {
        if (!this.isInCreativeTab(tab)) return;

        subItems.add(EMPTY_STACK);

        final FluidStack fluidStack = new FluidStack(ModFluids.SLIME, CAPACITY);
        final ItemStack stack = new ItemStack(this);
        final IFluidHandlerItem fluidHandler = stack.getCapability(CapabilityFluidHandler.FLUID_HANDLER_ITEM_CAPABILITY, null);
        if (fluidHandler != null)
        {
            final int fluidFillAmount = fluidHandler.fill(fluidStack, true);
            if (fluidFillAmount == fluidStack.amount) 
            {
                final ItemStack filledStack = fluidHandler.getContainer();
                subItems.add(filledStack);
            }
        }

    @Override
    public String getItemStackDisplayName(final ItemStack stack) 
    {
        String unlocalizedName = this.getUnlocalizedNameInefficiently(stack);
        IFluidHandlerItem fluidHandler = FluidUtil.getFluidHandler(stack);
        FluidStack fluidStack = fluidHandler.getTankProperties()[0].getContents();

        // If the bucket is empty, translate the unlocalised name directly
        if (fluidStack == null || fluidStack.amount <= 0) 
        {
            unlocalizedName += ".name";
        }
        else
        {
            unlocalizedName += ".filled.name";
        }

    return I18n.translateToLocal(unlocalizedName).trim();
    }

Basically the code above creates and empty and full sub-item and also sets the display name so that it is different when empty. If you wanted to have items and names for other fluid levels, hopefully you can figure out how to modify the code accordingly.

Step 3c. @Override The onRightClick() Method To Perform Desired Fluid-Related Actions

Okay, this part is honestly fairly tricky and really depends on what you want your particular item to do. For example, you might want your item to work like a bucket where you can fill it by right-clicking on a fluid block, and conversely empty the bucket and place a fluid block by right-clicking on a suitable placement position. However, it is possible you'd want to do something different, like when your item is full of poison it might transform what you touch with it, or maybe it could be a weapon that infects people with a virus. It is up to you.

Even for something as simple as filling and emptying on fluid blocks, there is a lot of code to write to handle all the possible cases. Like if you're in creative mode your item shouldn't drain, and you need to manage all the cases where the capacity of the item and the amount to transfer may not match, and so forth.

Warning: Although the fluid handler and the FluidUtil classes contain methods such as fill() and drain() to aid in filling and draining, I found they are quite confusing in their operation because they do things like returning copies of the item stack, they can be executed as a "simulation" without actually transferring fluid and such. So I personally code it from scratch, but you may also want to look at the UniversalBucket or other mods' code for inspiration.

Here is an example where I am able to place and fill my item. Note it is really long, but that is because I try to handle all the cases I can: see github link containing the code. There are lots of comments so you should be able to understand it, but the main point is I:

  • Handle a bunch of cases where the information is invalid (null or out of range).
  • Check a bunch of conditions until I'm sure a fluid transfer should happen, either a fill or drain.
  • Calculate the amount of fluid to transfer and update both fluid handlers (in the item and in the block).
  • Update the fluid block (if I'm filling my item then I can empty the block location, if I'm placing fluid it places a block).
  • Make any sounds associated with filling or draining.
  • Update the item's NBT data.
  • Send an inventory update to the player.

As mentioned above, it is quite possible you want to do something entirely different. So just code it carefully and think of all the possible situations you need to handle.

Tip: You'll notice in my code that I'm meticulous in organizing my if-else statements very thoroughly and completely. I always put an else for each if because then I can make sure every possible situation is thoughtfully handled. I also don't generally combine conditions because I find debugging is a lot easier if you test one condition at a time.

Step 4. Create A Singleton Instance Of Your Custom Item

Somewhere, best in either your main mod class or a ModItems collection class, you should create an instance of your custom item that you can reference throughout your code. Some people recently are using the @ObjectHolder injection method for their instances but I prefer to just do them directly, but either way will work.

For example, in my ModItems class I have:

public final static ItemSlimeBag SLIME_BAG = new ItemSlimeBag();

Step 5. Register Your Item Instance Normally

Register your item instance normally. In modern versions of Forge you should use subscribe to the RegistryEvent.Register event. I will assume you know how to register items.

Step 5. Make A Custom Class That Extends IModel, Along With A Custom IBakedModel, ModelLoader And ItemOverrideList

In most cases you'll want the texture and maybe the model itself to change based on the fill level of your item. Therefore, you will want to use the baked model system. "Baking" simply means taking the model description (from JSON and such) and pre-processing it so that it is ready to be used by the computers GPU (Graphical Processing Unit).

During the mod loading there is a point where the model baking will occur, and it will look for a custom model loader and then bake a model version for each variant of your item.

Typically I put all the related classes together in one file, for example see this github link containing my code for a model for a fluid-handling item.

Important: Feel free to copy much of this code, but the key things to modify are everywhere I have something related to "slime": resource locations for the textures, where I look for match to my fluid and such.

Tip: I have a lot of console print statements in this code, and you can learn a lot by watching for these statements when you eventually are able to run your mod -- it will give you a good understanding of the timing of when the baking occurs and which order the various methods fire.

You'll notice that the example model file I give uses a "cache" system to improve performance even further. You need to remember that the model gets accessed every single frame of rendering of the game -- this can be from 60 times a second and even 120 or more with very good computer monitors. So you want to optimize the performance as best you can.

Step 7. Register Your Custom ModelLoader, IModel and Item Variants

In a method that handles the ModelRegistryEvent event you need to register the model loader, the model and the item variants. For example:

ModelLoaderRegistry.registerLoader(ModelSlimeBag.CustomModelLoader.INSTANCE);
ModelLoader.setCustomMeshDefinition(SLIME_BAG, stack -> ModelSlimeBag.LOCATION);
ModelBakery.registerItemVariants(SLIME_BAG, ModelSlimeBag.LOCATION);

Note: SLIME_BAG here refers to the instance you created in Step #3. If your instance is in another class, then you will need to refer to it properly.

Note: the CustomModelLoader class is defined in my example code in the previous step, as is the LOCATION field.

Step 8. Put Texture Assets With Proper Names In Proper Resource File Locations

So you should put texture assets (.PNG usually) in the resource locations pointed to in your model file, in my example these were the emptyLocation and filledLocation field values.

Conclusion


Of course feel free to add additional functionality to your item. For example, maybe it can be used in a recipe, or maybe there are recipes that can produce it.

If you follow the above steps you should have a working fluid-handling item. If you chose to let it show up on the creative tab, then you can test your item by going into creative and getting the full and empty versions of the item. Then you should switch to survival mode and use the full one to place a block (and an empty bucket should return) and use the empty bucket(s) to fill up by clicking on the source block for the fluid.

Hope you liked this. Happy Modding!

No comments:

Post a Comment