Butterfly API is my Fabric 1.20.1 helper/API mod. It is mostly here so I do not have to keep rewriting the same registration helpers, item group setup, math utilities, and plush code every time I make another mod.
It also comes with a few actual things in-game, because I can and I did.
The short version: use this if you want some cleaner helper methods for Fabric modding, or if you want to add plushes that use Butterfly API's shared plush system.
- Minecraft 1.20.1
- Java 17
- Fabric Loader 0.16.10 or newer
- Fabric API 0.92.6+1.20.1 or newer
- GeckoLib 4.4.9 or newer
Build the mod jar:
.\gradlew.bat buildThe finished jar ends up in build/libs/.
Butterfly API is published on Modrinth under the slug butterfly-api. If you want to use it in another Fabric mod project, add Modrinth Maven and depend on version 1.0.0.
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
filter {
includeGroup "maven.modrinth"
}
}
}
dependencies {
modImplementation "maven.modrinth:butterfly-api:1.0.0"
}If your mod needs Butterfly API to be installed alongside it, add it to your fabric.mod.json too:
{
"depends": {
"butterfly_api": ">=1.0.0"
}
}Small warning because this one is easy to forget: Modrinth Maven does not bring in transitive dependencies. Keep Fabric API, GeckoLib, and anything else your dev environment needs declared separately :D
If you are working from source, publish it to your local Maven cache:
.\gradlew.bat publishToMavenLocalThen another local Gradle project can use it like this:
repositories {
mavenLocal()
maven { url = "https://maven.fabricmc.net/" }
maven {
name = "GeckoLib"
url = "https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/"
}
}
dependencies {
modImplementation "moth.butterflyapi:Butterfly API:1.0.0"
}Most of the API starts with a ModContext. Make one for your mod and then use it for ids, logging, registration, tabs, client helpers, and plush builders.
public final class ExampleMod implements ModInitializer {
public static final ModContext MOD = ButterflyApi.mod("example_mod", "Example Mod");
public static final Item EXAMPLE_ITEM = MOD.item("example_item", new Item(new Item.Settings()));
public static final Block EXAMPLE_BLOCK = MOD.block(
"example_block",
new Block(AbstractBlock.Settings.create().strength(1.5F))
);
@Override
public void onInitialize() {
MOD.addTo(ItemGroups.INGREDIENTS, EXAMPLE_ITEM);
MOD.logger().info("Loaded {}", MOD.modName());
}
}Useful context bits:
MOD.id("path")makesexample_mod:path.MOD.modId()gives the mod id.MOD.modName()gives the display name.MOD.logger()gives an SLF4J logger for the mod.MOD.registrar(),MOD.tabs(), andMOD.client()expose the helper classes directly.
This is the "what am I actually here for?" map. The code has the tiny details, but this should help you find the right part without reading every file first.
| If you want to... | Start with... | Good for... |
|---|---|---|
| Register normal mod stuff | ModContext / Registrar |
Items, blocks, sounds, enchantments, entities, particles, paintings, recipes, and similar |
| Make or fill creative tabs | TabBuilder / MOD.tabBuilder(...) |
Searchable tabs, categories, custom slot surfaces, per-entry styling, and adding entries to vanilla tabs |
| Do client-only setup | ClientRegistrar / MOD.client() |
Render layers, entity renderers, block entity renderers, screens, model predicates, particles |
| Do gameplay math | moth.butterflyapi.math |
Look vectors, particle circles, spread patterns, hitboxes, knockback, pushes, pulls, and value mapping |
| Add placeable plushes | PlushEntrypoint / PlushRegistrar |
Simple plushes, GeckoLib plushes, honk sounds, squish behavior, shared plush block entity setup |
| Target all plushes at once | ButterflyApiTags / #butterfly_api:plushes |
Code checks, datapack rules, loot logic, recipes, or anything that should care about every plush |
| Stop init code from running twice | Bootstrap / RunOnce |
Content bootstraps, lazy setup, and "please only do this once" code |
| Use the built-in content | butterfly_api ids |
Plushes, Matchstick, Big Red Button, Connection enchantment, creative tab content |
This part saves from typing Registry.register(...) over and over. Everything registers under your ModContext mod id.
public static final Item GEM = MOD.item("gem", new Item(new Item.Settings()));
public static final Block STONE_TILE = MOD.block(
"stone_tile",
new Block(AbstractBlock.Settings.create().strength(2.0F))
);
public static final Block LOGIC_BLOCK = MOD.block(
"logic_block",
new Block(AbstractBlock.Settings.create()),
registeredBlock -> new BlockItem(registeredBlock, new Item.Settings().maxCount(16))
);
public static final Block INVISIBLE_TECH_BLOCK = MOD.blockOnly(
"invisible_tech_block",
new Block(AbstractBlock.Settings.create().noCollision())
);
public static final SoundEvent CHIME = MOD.sound("chime");The registry helpers cover:
register(registry, path, value)for any registry.item(path, item).block(path, block)with a normalBlockItem.block(path, block, itemSettings)for a block with a custom item.block(path, block, BlockItemFactory)when you want full control.blockOnly(path, block)for blocks without items.door(path, block)forTallBlockItem.sound(path)andsound(path, soundEvent).enchantment,entity,blockEntityType,screenHandler,statusEffect,potion,recipeSerializer,recipeType,particle, andpainting.
The Tabs helper still supports the original MOD.tab(...) overloads, but the builder is where the more advanced creative-tab setup lives.
Start a builder through either MOD.tabBuilder("path") or MOD.tabs().builder("path"):
public static final ItemGroup EXAMPLE_TAB = MOD.tabBuilder("example")
.icon(EXAMPLE_ITEM)
.translationKey("itemGroup.example_mod.example")
.add(EXAMPLE_ITEM, EXAMPLE_BLOCK)
.build();Tabs created through the builder have a search bar by default. Search results stay scoped to the selected tab instead of turning into the global vanilla search tab.
The original helpers are still available:
public static final ItemGroup SIMPLE_TAB = MOD.tab(
"simple",
EXAMPLE_ITEM,
EXAMPLE_ITEM,
EXAMPLE_BLOCK
);
@Override
public void onInitialize() {
MOD.addTo(ItemGroups.BUILDING_BLOCKS, EXAMPLE_BLOCK);
}Useful tab entry points:
MOD.tabBuilder("example")starts aTabBuilder.MOD.tabs().builder("example")does the same thing through theTabshelper.MOD.tabKey("example")makes the item-group registry key.MOD.tabTranslationKey("example")returnsitemGroup.<modid>.example.MOD.tab(...)keeps the original simple tab overloads.MOD.addTo(groupKey, entries...)adds items or blocks to an existing tab.
public static final ItemGroup EXAMPLE_TAB = MOD.tabBuilder("example")
.icon(EXAMPLE_ITEM)
.displayName(Text.literal("Example"))
.entries(entries -> {
entries.add(EXAMPLE_ITEM);
entries.add(EXAMPLE_BLOCK);
})
.noScrollbar()
.noRenderedName()
.build();The builder supports:
icon(ItemConvertible)oricon(Supplier<ItemStack>).displayName(Text)ortranslationKey(String).entries(Consumer<ItemGroup.Entries>).add(ItemConvertible...).searchBar()when you want to state the searchable behavior explicitly.special().noScrollbar().noRenderedName().backgroundSuffix(String)for a vanilla creative background suffix.backgroundTexture(Identifier)for a full namespaced background texture.build()to register and return the finishedItemGroup.
A tab can replace the creative background and customize the slot grid:
public static final ItemGroup EXAMPLE_TAB = MOD.tabBuilder("example")
.icon(EXAMPLE_ITEM)
.backgroundTexture(MOD.id(
"textures/gui/container/creative_inventory/tab_example.png"
))
.slotBackgroundTexture(MOD.id("textures/gui/example_slot.png"))
.betweenSlotsColor("#30283D")
.tabNameColor("#F4D7FF")
.add(EXAMPLE_ITEM, EXAMPLE_BLOCK)
.build();Custom appearance helpers include:
customOverlay(true)orcustomAppearance()to use Butterfly API's custom grid rendering.customOverlay(false)orvanillaAppearance()to keep the vanilla tab appearance.slotBackgroundTexture(Identifier)orslotBackgroundColor("#RRGGBB").betweenSlotsTexture(Identifier)orbetweenSlotsColor("#RRGGBB").tabNameColor("#RRGGBB")anddefaultTabNameColor().
A vanilla-looking tab can still receive a tint:
public static final ItemGroup TINTED_TAB = MOD.tabBuilder("tinted")
.icon(EXAMPLE_ITEM)
.vanillaTint("#D77FA6")
.add(EXAMPLE_ITEM)
.build();Tinting is intentionally limited to vanilla appearance. This keeps it as the lightweight option for a tab that should still look vanilla instead of mixing it with the full custom overlay.
Supported tint forms:
.vanillaTint("#D77FA6")
.vanillaTint("#D77FA6", 0.25F)
.vanillaAppearance().tint("#40D77FA6")Six-digit tint colors use Butterfly API's default transparency. Eight-digit tint colors use #AARRGGBB.
Categories reserve a full horizontal row, display a heading, and place their entries beneath it. Entries added directly to the main builder stay uncategorized and appear before the categories.
public static final ItemGroup EXAMPLE_TAB = MOD.tabBuilder("example")
.icon(EXAMPLE_ITEM)
.add(UNCATEGORIZED_ITEM)
.category("building", Text.literal("Building"), category -> category
.alignLeft()
.backgroundTexture(MOD.id("textures/gui/building_category.png"))
.borderColor("#5B435F")
.textColor("#FFFFFF")
.add(EXAMPLE_BLOCK))
.category("items", Text.literal("Items"), category -> category
.alignCenter()
.backgroundColor("#93678F")
.add(EXAMPLE_ITEM))
.build();Category helpers include:
alignLeft(),alignCenter(), andalignRight().textAlignment(TabCategory.TextAlignment).backgroundTexture(Identifier)orbackgroundColor("#RRGGBB").borderColor("#RRGGBB").textColor("#RRGGBB").entries(...),add(...), and the same styled-entry helpers as the main builder.emptySlot()for one deliberately empty position.emptyRow()for a full cushion row.
Categories appear in registration order unless categoryOrder(...) is used to provide an explicit order. A category with no matching entries is hidden while searching.
Normal entries automatically use the tab-wide slot texture or color:
.add(EXAMPLE_ITEM)A specific entry can override that surface:
.entry(EXAMPLE_ITEM, entry -> entry
.slotColor("#E68FB3"))Texture overrides work the same way:
.entry(EXAMPLE_BLOCK, entry -> entry
.slotTexture(MOD.id("textures/gui/example_block_slot.png")))The longer method names are also available:
.entry(EXAMPLE_ITEM, entry -> entry
.slotBackgroundColor("#E68FB3"))
.entry(EXAMPLE_BLOCK, entry -> entry
.slotBackgroundTexture(MOD.id("textures/gui/example_block_slot.png")))The same styling API is available inside categories and for search-only entries.
Search-only entries do not appear while the search field is empty. They become visible only when the current search matches them.
public static final ItemGroup EXAMPLE_TAB = MOD.tabBuilder("example")
.icon(EXAMPLE_ITEM)
.add(EXAMPLE_ITEM)
.searchOnly(SECRET_ITEM)
.searchOnlyEntry(SECRET_BLOCK, entry -> entry
.slotColor("#6B4C8C"))
.build();Normal entries do not require any extra code. Keep using add(...), entries(...), or entry(...) unless something should specifically be hidden until searched.
Empty positions still render the configured slot texture or color:
public static final ItemGroup EXAMPLE_TAB = MOD.tabBuilder("example")
.icon(EXAMPLE_ITEM)
.add(EXAMPLE_ITEM)
.emptySlot()
.add(EXAMPLE_BLOCK)
.emptyRow()
.add(ANOTHER_ITEM)
.build();emptySlot()reserves one slot.emptyRow()finishes the current row and adds a full empty cushion row.- Empty positions keep the normal slot hover overlay.
- Category heading rows do not show individual slot hover overlays.
Butterfly API stretches supplied textures to the expected UI area, so larger textures with the same ratio also work.
Recommended native sizes:
- Full creative background:
195x136. - Slot texture:
16x16. - Category texture:
160x16. - Vanilla search field width:
80.
A category texture uses a 10:1 ratio. 320x32 or 640x64 will scale correctly, while a different ratio will be stretched to fit.
Example asset paths:
assets/example_mod/textures/gui/container/creative_inventory/tab_example.png
assets/example_mod/textures/gui/example_slot.png
assets/example_mod/textures/gui/example_category.png
Client setup gets its own little wrapper too. Use these from your client entrypoint.
public final class ExampleClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ExampleMod.MOD.cutout(ExampleMod.EXAMPLE_BLOCK);
ExampleMod.MOD.entityRenderer(ExampleMod.EXAMPLE_ENTITY, ExampleEntityRenderer::new);
ExampleMod.MOD.blockEntityRenderer(ExampleMod.EXAMPLE_BLOCK_ENTITY, ExampleBlockEntityRenderer::new);
}
}Client helpers include:
cutout,cutoutMipped, andtranslucentrender layers.entityRendererfor entity renderers.blockEntityRendererfor block entity renderers.screenfor handled screens.predicatefor item model predicates.modelLayerfor entity model layers.particleFactoryfor particle factories.
The math package is a pile of small helpers that are useful when working with particles, movement, targeting, hitboxes, and general "where is this thing in 3D space" code.
Scalars is for tiny number helpers:
clamp01squareandcubeisZeroandnearlyEquallerpandinverseLerpmapandmapClampedapproachroundToEPSILON
Vecs is for Vec3d work:
safeNormalize(vector)andsafeNormalize(vector, fallback).horizontal,withX,withY,withZ.addX,addY,addZ.scaleX,scaleY,scaleZ.midpoint,lerp,toward,direction.setLength,limitLength,clampLength.projectOnto,rejectFrom,reflect,perpendicular.closestPointOnSegment,distanceToSegment,distanceSqToSegment.lengthSquared.
Angles and YawPitch are for turning rotations into vectors and vectors back into rotations.
- degrees/radians conversion.
- degree and radian wrapping.
- shortest-angle deltas.
- degree lerp that handles wraparound properly.
direction(yaw, pitch).yaw,pitch,yawPitch, andlookAt.
Vec3d direction = Angles.direction(player.getYaw(), player.getPitch());
YawPitch look = Angles.lookAt(player.getEyePos(), target.getEyePos());Basis3 makes a local forward/right/up basis from a direction. Sampling makes ring, circle, arc, and spread points. Boxes makes common Box shapes. Motion handles velocity, impulses, knockback, pushes, and pulls.
Vec3d forward = Vecs.direction(player.getEyePos(), target.getEyePos());
List<Vec3d> ring = Sampling.circle(target.getPos(), forward, 2.0D, 16);
Box traceBox = Boxes.between(player.getEyePos(), target.getEyePos(), 0.25D);
Motion.pushFrom(target, player, 0.8D, 0.25D);This is the fun part, but it is still an actual API. Butterfly API has a shared plush system for small placeable plush blocks, so other mods can add plushes without each one needing its own block entity setup.
Plushes are:
- Registered through the
butterfly_api:plushentrypoint before plush bootstrap finishes. - Bound into one shared
butterfly_api:plushblock entity type. - Waterloggable and horizontally facing.
- Clickable, with a mode toggle, honk sound, and squish effect.
- Rendered with either a normal block model or GeckoLib.
- Customizable with block settings, item settings, sounds, animation controllers, render scale/offset, and use behavior.
Add a plush entrypoint in fabric.mod.json:
{
"entrypoints": {
"butterfly_api:plush": [
"example.ExamplePlushes"
]
}
}Register plushes from that entrypoint:
public final class ExamplePlushes implements PlushEntrypoint {
private static final ModContext MOD = ButterflyApi.mod("example_mod", "Example Mod");
@Override
public void registerPlushes(PlushRegistrar registrar) {
registrar.plush(MOD, "plain_plush");
registrar.plush(MOD, "custom_plush", builder -> builder
.sound("custom_plush_honk")
.itemSettings(new Item.Settings().maxCount(16))
.onUse(plush -> plush.squish(2)));
registrar.geckoPlush(MOD, "animated_plush", builder -> builder
.loopingAnimation("idle")
.geckolib(PlushDefinition.GeckoRenderData
.block(MOD, "animated_plush")
.withScale(0.9F)
.withOffset(0.0F, 0.0F, 0.0F)));
}
}Default plush conventions:
- Default sound id is
<namespace>:<path>_honk. - Normal plushes use normal blockstate, block model, item model, and texture assets.
- Gecko plushes default to:
assets/<modid>/geo/block/<path>.geo.jsonassets/<modid>/textures/block/<path>.pngassets/<modid>/animations/block/<path>.animation.json
After bootstrap, a registered plush gives you its block, item, and sound:
RegisteredPlush plush = ButterflyPlushes.get(MOD.id("plain_plush"));
Block block = plush.block();
Item item = plush.item();
SoundEvent sound = plush.sound();Register plushes through the entrypoint path. Once ButterflyPlushes.bootstrap() has run, the shared block entity type is already built and new plushes cannot be added.
Butterfly API exposes plush tags for code and datapacks.
if (stack.isIn(ButterflyApiTags.PLUSH_ITEMS)) {
// This item is a Butterfly API plush item.
}Code helpers:
ButterflyApiTags.PLUSH_BLOCKSButterflyApiTags.PLUSH_ITEMSButterflyApiTags.block("path")ButterflyApiTags.item("path")
Datapack ids:
#butterfly_api:plushesfor item tags.#butterfly_api:plushesfor block tags.
The bundled plush tag currently contains:
butterfly_api:syntax_plushbutterfly_api:hex_plushbutterfly_api:hex_maid_plushbutterfly_api:moth_plushbutterfly_api:chicken_plushbutterfly_api:capozi_plushbutterfly_api:debug_plush
RunOnce and Bootstrap are tiny helpers for init code that should only run once.
private static final Bootstrap BOOTSTRAP = Bootstrap.create();
public static void init() {
BOOTSTRAP.run(
ModItems::init,
ModBlocks::init,
ModSounds::init
);
}Bootstrap.run(...) returns true the first time it runs and false after that. hasRun() tells you whether it already fired.
Butterfly API is not only helper classes. It also ships some content under the butterfly_api namespace.
Bundled plushes:
- Syntax Plush
- Hex Plush
- Hex Maid Plush
- Moth Plush, using GeckoLib animation
- Chicken Plush
- Capozi Plush
- Debug Plush
Each plush is placeable, interactable, tagged as a plush, has a honk sound, and appears in the Butterfly API creative tab. There is also a collect_all_plushes advancement.
butterfly_api:big_red_button is exactly what it sounds like. It can be placed on floors, walls, or ceilings, and it tracks each player's clicks.
It grants advancements at:
- 1 click
- 100 clicks
- 10,000 clicks
- 1,000,000 clicks
butterfly_api:matchstick is a small fire-starting item. It can:
- Light campfires, candles, and candle cakes.
- Place
matchstick_fireormatchstick_soul_fire. - Light Nether portals when used on a valid frame.
- Set living entities on fire.
butterfly_api:connection is a treasure enchantment for enchantable items. It binds to an owner when enchanted or taken from an anvil.
Connected items:
- Can only be picked up or taken by their owner after binding.
- Are preserved through death up to
ConnectionEnchantmentUtil.MAX_CONNECTION_DEATHS. - Can appear on books from trail ruins archaeology loot.
butterfly_api:hipainting variant.- Butterfly API creative tab.
- Crafting recipes for bundled plushes and the Big Red Button.
- Extra datapack recipes, including smithing template duplication recipes.
If you want to dig through the source, the packages are split up like this:
- API entrypoints live in
moth.butterflyapi. - Registry helpers live in
moth.butterflyapi.registry. - Mod context helpers live in
moth.butterflyapi.mod. - Client helpers live in
moth.butterflyapi.client. - Item group helpers live in
moth.butterflyapi.itemgroup. - Math helpers live in
moth.butterflyapi.math. - Plush helpers live in
moth.butterflyapi.plush. - Bundled content lives in
moth.butterflyapi.content.