Forks, Branches, and Scrolls

I’ve finally released the butterfly mod, but I’m still not done. Now I’m working on the next release which will have a few extra features. This week I’ve been working on Butterfly Scrolls – a new way to create a collection of butterflies.

Forks


Last week I released the first version of Bok’s Butterflies. There were a few different websites I used:

I also added a post to the Minecraft Forum after finishing last week’s article.

Modrinth initially rejected my mod, asking for verification that I owned the mod, or had the rights to distribute it. After leaving a message for them both from my GitHub account and on this site, they approved it and you can now also download it from there. This seems like a normal part of the process – it takes a bit longer, but at least you can know they check these things.

I got the most traction from CurseForge with over 200 downloads so far. Nothing to write home about, but more than any of the other locations, which each have less than 20 downloads. Three people on CurseForge have already added Bok’s Butterflies to their modpacks (Forsaken Worlds, Majula, and SteinCraft). These aren’t the most popular modpacks, but it’s nice to know some people actually wanted to include it.

By far the best compliment is that someone forked me. One GitHub user created a fork of my source code and has ported it to Fabric! They didn’t just want the mod, but they were willing to put in extra effort to be able to use it with a different mod loader. It also saves me some work, since there’s already a Fabric version out there.

All-in-all the mod isn’t that popular, yet. But it’s still a bit more popular that I was expecting it to be.

Branches


Now that I have a release, I want to be a little more careful with the source code. Up until now I have been doing all my work in a development branch, and then pushing it straight into main. I can’t do this from this point moving forward.

The problem will come if someone reports a bug with the release. If I have been modifying master, then in order to fix the bug, I’ll first need to make sure there are no new bugs in my current code. I’ll then have to fix the bug in question, and then I’ll have to release the fix with all the incomplete features I’m still working on. People may end up having to wait for the next stable release to be able to play the mod again.

To prevent this, I have created a release branch from master. This branch serves as a snapshot of the code at the time of release. If I need to fix anything, I can merge it into this branch instead and create a release from there.

I’m also not going to be working in the development branch going forward. I’ll create a branch for each new feature, and once that is completed I will merge it into the development branch instead. Only when the next release is ready will I merge the code into master.

So we now have four kinds of branches:

  • Main
    The latest code that has been released.
  • Release
    A snapshot of the code at a specific version, with any fixes it might need.
  • Development
    The latest version of features currently still in development.
  • Feature
    Code for a feature that is currently being developed.

And with that I created a feature branch for the Butterfly Scrolls.

Scrolls


The next feature I wanted to work on was a new way of collecting butterflies. Essentially, the crueler Minecraft players can pin the butterflies to a piece of paper to create a Butterfly Scroll. They can then look at the scroll to see the butterfly they pinned to it.

This is a new kind of item for me, since we need a UI element similar to how a book works. So I started digging through the source code in order to figure out how to make this work.

Screen

UI elements in Minecraft inherit from the Screen. They handle the rendering of the UI element, as well as any widgets that are used to control the UI. Our screen is very simple: it just shows an image, and gives an option to close it. We create a class that only exists in Dist.CLIENT, and give it a butterfly index as a parameter.

/**
 * The GUI screen for a butterfly page.
 */
@OnlyIn(Dist.CLIENT)
public class ButterflyScrollScreen extends Screen {

    private final int butterflyIndex;

    /**
     * Constructs a basic butterfly page screen.
     */
    public ButterflyScrollScreen(int butterflyIndex) {
        super(GameNarrator.NO_TITLE);

        this.butterflyIndex = butterflyIndex;
    }
}

To render the screen, we need locations for the textures. The butterfly index tells us which one to use. We then just use the blit() method in render() to draw the texture.

    /**
     * The location of the screen textures.
     */
    public static final ResourceLocation[] TEXTURES =
            {
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/admiral.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/buckeye.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/cabbage.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/chalkhill.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/clipper.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/common.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/emperor.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/forester.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/glasswing.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/hairstreak.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/heath.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/longwing.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/monarch.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/morpho.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/rainbow.png"),
                    new ResourceLocation("butterflies", "textures/gui/butterfly_scroll/swallowtail.png")
            };

    /**
     * Render the screen.
     * @param guiGraphics The graphics buffer for the gui.
     * @param x The x position of the cursor.
     * @param y The y position of the cursor.
     * @param unknown Unknown.
     */
    public void render(@NotNull GuiGraphics guiGraphics, int x, int y, float unknown) {
        this.renderBackground(guiGraphics);
        int i = (this.width - 192) / 2;
        guiGraphics.blit(TEXTURES[this.butterflyIndex], i, 2, 0, 0, 192, 192);
        super.render(guiGraphics, x, y, unknown);
    }

The last thing we need is a way to exit the screen. We can just use a default UI widget available in the vanilla source code.

    /**
     * Called when the screen initialises.
     */
    @Override
    protected void init() {
        super.init();
        this.createMenuControls();
    }

    /**
     * Creates a close button for the screen.
     */
    protected void createMenuControls() {
        this.addRenderableWidget(Button.builder(CommonComponents.GUI_DONE, (button) ->
                this.onClose()).bounds(this.width / 2 - 100, 196, 200, 20).build());
    }

That’s our screen complete! Now we need an item to access it.

Scroll Item

The scroll item is fairly simple. We inherit from ButterflyContainerItem so that we can use its appendHoverText for the item’s description.

public class ButterflyScrollItem extends Item implements ButterflyContainerItem {

    public static final String NAME = "butterfly_scroll";

    /**
     * Construction
     * @param itemProperties The properties of the item.
     */
    public ButterflyScrollItem(Item.Properties itemProperties) {
        super(itemProperties);
    }

    /**
     * Adds some helper text that tells us what butterfly is on the page (if any).
     * @param stack The item stack.
     * @param level The current level.
     * @param components The current text components.
     * @param tooltipFlag Is this a tooltip?
     */
    @Override
    public void appendHoverText(@NotNull ItemStack stack,
                                @Nullable Level level,
                                @NotNull List<Component> components,
                                @NotNull TooltipFlag tooltipFlag) {
        appendButterflyNameToHoverText(stack, components);
        super.appendHoverText(stack, level, components, tooltipFlag);
    }
}

To open the UI we use Minecraft.getInstance() to set the screen. The screen will then take over until the player closes it.

    /**
     * Open the GUI when the item is used.
     * @param level The current level.
     * @param player The current player.
     * @param hand The hand holding the item.
     * @return The interaction result (always SUCCESS).
     */
    @Override
    @NotNull
    public InteractionResultHolder<ItemStack> use(Level level,
                                                  Player player,
                                                  @NotNull InteractionHand hand) {
        ItemStack itemstack = player.getItemInHand(hand);

        if (level.isClientSide()) {
            CompoundTag tag = itemstack.getTag();
            if (tag != null && tag.contains(CompoundTagId.CUSTOM_MODEL_DATA)) {
                Minecraft.getInstance().setScreen(new ButterflyScrollScreen(tag.getInt(CompoundTagId.CUSTOM_MODEL_DATA)));
            }
        }

        player.awardStat(Stats.ITEM_USED.get(this));
        return InteractionResultHolder.sidedSuccess(itemstack, level.isClientSide());
    }

The Butterfly Scroll uses compound tags, both for telling the player what kind of butterfly is on the scroll, and to tell the screen what texture to use. We set these tags during crafting, similar to how we set them for Bottled Butterflies. In order keep our code organised, we move our setButterfly() method to the ButterflyContainerItem interface.

    /**
     * Transfer butterfly data when crafting items that contain a butterfly.
     * @param event The event data
     */
    @SubscribeEvent
    public static void onItemCraftedEvent(PlayerEvent.ItemCraftedEvent event) {

        ItemStack craftingItem = event.getCrafting();
        if (craftingItem.getItem() == ItemRegistry.BOTTLED_BUTTERFLY.get() ||
            craftingItem.getItem() == ItemRegistry.BUTTERFLY_SCROLL.get()) {

            Container craftingMatrix = event.getInventory();
            for (int i = 0; i < craftingMatrix.getContainerSize(); ++i) {

                ItemStack recipeItem = craftingMatrix.getItem(i);
                CompoundTag tag = recipeItem.getTag();
                if (tag != null && tag.contains(CompoundTagId.ENTITY_ID)) {

                    String entityId = tag.getString(CompoundTagId.ENTITY_ID);
                    ButterflyContainerItem.setButterfly(craftingItem, entityId);
                    break;
                }
            }
        }
    }

After this we make sure we register the item in all the usual places and we are almost done. We just need to make everything look pretty now.

Data and Textures

I created 16 textures for each scroll, based on the vanilla paper icon. I also created textures for each of the scrolls’ UI screens so we can actually use them. The textures for these include the iron ingot that has been used to pin them to the paper.

The only thing left are the usual recipes, as well as an achievement for creating a scroll. I added shapeless recipes using an iron nugget to represent the nail, paper to nail the butterfly to, and either a full net or bottle to get the butterfly from.

I haven’t created an advancement for collecting all 16 scrolls yet. I have a different feature planned for players who collect all the scrolls.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.