Skip to content

Detailed guide

Martijn Muijsers edited this page Feb 8, 2026 · 14 revisions

This is a detailed guide of what you can do with Fiddle, step by step.

Paper plugin

To use Fiddle, you must first have a Paper plugin. In particular, your plugin must have a bootstrapper:

public class MyPluginBootstrap implements PluginBootstrap {
    @Override
    public void bootstrap(BootstrapContext context) {
        // Do bootstrap stuff here
    }
}

Most functionality requires using Minecraft internals. Therefore, your plugin should have Fiddle as a dev bundle dependency. The dev bundle of Fiddle is currently not published anywhere, so you should publish it to your Maven local:

  • Clone the Fiddle repository and open a terminal in its root directory
  • ./gradlew applyPatches
  • (cd gradle-bin && ./publishDevBundleToMavenLocal)

Adding new content

Blocks

To add a new block, we must register it with its corresponding registry.

Simply register with the FiddleEvents.BLOCK event in your plugin's bootstrap method:

context.getLifecycleManager().registerEventHandler(
    FiddleEvents.BLOCK,
    event -> {
        // Register custom blocks here
    }
);

You can then get the registry and register a new block:

event.registry().register(
    TypedKey.create(RegistryKey.BLOCK, Key.key("example:ash_block")),
    builder -> {
        // Customize the block here
    }
);

While we could leave it at this, the resulting block will be very uninteresting. The builder (of type BlockRegistryEntry.Builder) allows us to customize the block before it is added.

Currently, there is no API to modify the block using Bukkit APIs only; you must change the properties using Minecraft internals directly.

To do that, you can cast builder to a super-secret interface BlockRegistryEntryBuilderNMS which exposes some useful methods:

  • propertiesNMS(..) allowing us to replace or modify the block's properties
  • factoryNMS(..) allowing us to use our own factory to turn the properties into a net.minecraft.world.level.Block instance

Typically, this means you do the following:

  • Make a factoryNMS call if you want to use a specific net.minecraft.world.level.Block type, such as StairBlock, AnvilBlock, FlowerPotBlock etc.
  • Make a propertiesNMS call to configure some properties

Here is an example for a regular (full-sized) block, for which we just define some properties:

var builderNMS = (BlockRegistryEntryBuilderNMS) builder;
builderNMS.propertiesNMS(properties -> {
    // It shows up light gray on maps
    properties.mapColor(MapColor.COLOR_LIGHT_GRAY);
    // It drops nothing unless broken with the right tool (a shovel, as defined in the included data pack)
    properties.requiresCorrectToolForDrops(); 
    // It breaks when pushed by a piston
    properties.pushReaction(PushReaction.DESTROY);
});

Here is an example of using a custom factory to add a stairs block:

event.registry().register(
    TypedKey.create(RegistryKey.BLOCK, Key.key("example:ash_stairs")),
    builder -> {
        var builderNMS = (BlockRegistryEntryBuilderNMS) builder;
        // Get the full block for these stairs
        var fullBlock = BuiltInRegistries.BLOCK.getValue(Identifier.parse("example:ash"));
        // Use a factory that returns StairBlock
        builderNMS.factoryNMS(properties ->
            new StairBlock(fullBlock.defaultBlockState(), properties) {}
        ); 
    }
);

Here is an example of a custom netherite anvil:

event.registry().register(
    TypedKey.create(RegistryKey.BLOCK, Key.key("example:netherite_anvil")),
    builder -> {
        var builderNMS = (BlockRegistryEntryBuilderNMS) builder;
        // Use a factory that returns AnvilBlock
        builderNMS.factoryNMS(AnvilBlock::new); 
        // Customize some properties
        builderNMS.propertiesNMS(properties -> {
            properties.mapColor(MapColor.METAL);
            properties.requiresCorrectToolForDrops();
            properties.strength(5.0F, 1200.0F);
            properties.sound(SoundType.ANVIL);
            properties.pushReaction(PushReaction.BLOCK);
        });
    }
);

Items

Adding items works the same as adding blocks: register with the FiddleEvents.ITEM_REGISTRY_COMPOSE event, get the registry and add the new items:

context.getLifecycleManager().registerEventHandler(
    FiddleEvents.ITEM,
    event -> {
        event.registry().register(
            TypedKey.create(RegistryKey.ITEM, Key.key("example:ash")),
            builder -> {
                // Customize the item here
            }
        );
    }
);

Similarly to blocks, the builder must currently be cast to ItemRegistryEntryBuilderNMS, which provides factoryNMS and propertiesNMS methods:

builder -> {
    var builderNMS = (ItemRegistryEntryBuilderNMS) builder;
    builderNMS.propertiesNMS(properties -> {
        // It stacks to 32
        properties.stacksTo(32);
        // It is resistant to fire
        properties.fireResistant();
        // It has epic rarity
        properties.rarity(Rarity.EPIC);
    });
}

Adding a block item requires providing the block, which is quite a hassle:

event.registry().register(
    TypedKey.create(RegistryKey.ITEM, Key.key("example:ash_block")),
    builder -> {
        var builderNMS = (ItemRegistryEntryBuilderNMS) builder;
        // Get the block
        var block = BuiltInRegistries.BLOCK.getValue(Identifier.parse("example:ash_block"));
        builderNMS.factoryNMS(properties -> 
            new BlockItem(block, properties)
        );
        builderNMS.propertiesNMS(properties -> {
            // Copy the item name from the block name
            properties.useBlockDescriptionPrefix();
        });
    }
);

For block items, you can simply use the following shorthand that does the same as the above:

event.registry().register(
    TypedKey.create(RegistryKey.ITEM, Key.key("example:ash_block")),
    builder -> {
        var builderNMS = (ItemRegistryEntryBuilderNMS) builder;
        builderNMS.factoryForBlockNMS(); // Woohoo so much shorter
    }
);

Data pack

To customize the way in which your custom blocks and items behave, you can use a data pack. With a data pack, you can add block tags (such as #minecraft:mineable/shovel, #minecraft:anvil), drop tables for blocks, crafting recipes and more.

It is recommended to put your data pack in your plugin's src/main/resources/data_pack folder. For example, you could have the following structure:

src/
  main/
    resources/
      data_pack/
        pack.mcmeta
        data/
          example/
            recipe/
              ash_block.json
              ash_stairs.json
          minecraft/
            tags/
              block/
                mineable/
                  shovel.json

To load your data pack, put this in your plugin's bootstrap method:

context.getLifecycleManager().registerEventHandler(
    LifecycleEvents.DATAPACK_DISCOVERY,
    event -> {
        try {
            event.registrar().discoverPack(this.getClass().getResource("/data_pack").toURI(), "provided");
        } catch (URISyntaxException | IOException e) {
            throw new RuntimeException(e);
        }
    }
);

Mapping custom blocks and items

The vanilla client doesn't know about your custom blocks and items. Therefore, we must map the packets sent to the client so that our custom blocks and items are as visible as possible to them while only using vanilla elements.

Blocks

Before sending any block to the client, Fiddle allows you to map it to a different block.

You can influence how a block looks on the client by registering a mapping. To do this, register with the FiddleEvents.BLOCK_MAPPING event and add the desired mappings:

context.getLifecycleManager().registerEventHandler(
    FiddleEvents.BLOCK_MAPPING,
    event -> {
        // Register mappings here
    }
);

Mappings are added by calling register(..):

event.register(builder -> {
    // Define the mapping here
});

We can use the given builder instance to define our mapping from BlockData to BlockData:

event.register(builder -> {
    // From ash block
    builder.from(Registry.BLOCK.get(Key.key("example:ash_block")).createBlockData());
    // To light gray concrete powder
    builder.to(BlockType.LIGHT_GRAY_CONCRETE_POWDER.createBlockData());
});

Since mappings are from BlockData to BlockData, you have to call .createBlockData() to get the default block data for a BlockType. This is a bit inconvenient, so there are also the following shorthands:

// From ash block
builder.fromEveryStateOf(Registry.BLOCK.get(Key.key("example:ash_block")));
// To light gray concrete powder
builder.toDefaultStateOf(BlockType.LIGHT_GRAY_CONCRETE_POWDER);

There is also a registerStateToState(..) shorthand to map every state of a block to the equivalent state of another block:

event.registerStateToState(
    // From every block state of ash stairs
    Registry.BLOCK.get(Key.key("example:ash_stairs")),
    // To the corresponding state of andesite stairs
    BlockType.ANDESITE_STAIRS
);

If you want to apply mappings for a specific type of player, you can use awarenessLevel(...) to pick a category of client:

event.register(builder -> {
    // Only applies to player that have the resource pack
    builder.awarenessLevel(ClientView.AwarenessLevel.RESOURCE_PACK);
    // From ash block
    builder.fromEveryStateOf(Registry.BLOCK.get(Key.key("example:ash_block")));
    // To a particular note block state
    NoteBlock noteBlockState = BlockType.NOTE_BLOCK.createBlockData();
    noteBlockState.setInstrument(Instrument.BELL);
    noteBlockState.setNote(Note.natural(0, Note.Tone.G))
    builder.to(noteBlockState);
});

ClientView is a description of the client. In particular, ClientView.AwarenessLevel is an enum that puts the player into one of the following categories:

  • VANILLA - Client without the resource pack
  • RESOURCE_PACK - Client with the generated resource pack
  • CLIENT_MOD - (Does not exist yet) Potential future client with a mod that adds the Fiddle blocks and items client-side

When registering a mapping, it is best to select which AwarenessLevels the mapping should apply to. For example, you could map example:ash_block like this:

  • VANILLA: To minecraft:light_gray_concrete_powder
    (looks the most similar)
  • RESOURCE_PACK: To minecraft:note_block[instrument=bell,note=1,powered=false]
    (with the texture for that note block state being overridden in the resource pack)
  • CLIENT_MOD: No mapping is applied, the client receives minecraft:ash_block unchanged

By default, block mappings will apply to VANILLA and RESOURCE_PACK.

If you want more control, you can also register a custom function for the mapping:

event.register(builder -> {
    builder.from(BlockTypes.GRASS_BLOCK.createBlockData());
    builder.to(
        handle -> {
            // Check that this block is physical (and not in a block display etc.)
            if (handle.getContext().isStateOfPhysicalBlockInWorld()) {
                // If it is above y = 128
                if (handle.getContext().getPhysicalBlockY() > 128) {
                    // Replace it by snowy grass
                    Snowable grassBlock = BlockType.GRASS_BLOCK.createBlockData();
                    grassBlock.setSnowy(true);
                    handle.set(grassBlock);
                }
            }
        },
        true // Whether this mapping uses coordinates
    );
});

This mapping gives you a handle (of type BlockMappingHandle) that you can use to perform your mapping. It provides the following methods:

  • getOriginal() - Returns the original BlockData
  • getImmutable() - Returns the current BlockData to be sent (which may be different from the original if another mapping already changed it)
  • set(BlockData) - To change the block state to be sent
  • getContext() - Returns a BlockMappingFunctionContext instance, which provides information about the context of the mapping

The BlockMappingFunctionContext allows you to get more context about the block itself (with isStateOfPhysicalBlockInWorld(), getPhysicalBlockX(), etc.). You can also use getClientView() to get a ClientView instance, which provides more detailed information about the player's client.

For example, you could use this mapping to send a pumpkin to players with a high ping:

builder.to(
    handle -> {
        var player = handle.getContext().getClientView().getPlayer();
        if (player != null && player.getPing() >= 200) {
            handle.set(BlockTypes.PUMPKIN.createBlockData());
        }
    },
    false // Whether this mapping uses coordinates
);

You should try to use only non-function mappings (using to(BlockData) and such), since they can be optimized by Fiddle internally. Function mappings may hurt performance, especially if they apply to commonly sent blocks.

Also, note that function mappings may or may not be run on the main thread. Making sure your code is thread-safe is your own responsibility.

Items

To map items, you can register with the FiddleEvents.ITEM_MAPPING event and add mappings:

context.getLifecycleManager().registerEventHandler(
    FiddleEvents.ITEM_MAPPING,
    event -> {
        // Register mappings here
    }
);

Mappings map from ItemType to a new ItemType:

// For all clients without a client mod
event.register(builder -> {
    builder.from(Registry.ITEM.get(Key.key("example:ash")));
    builder.to(ItemType.GUNPOWDER);
});

// Different per awareness level
event.register(builder -> {
    builder.awarenessLevel(ClientView.AwarenessLevel.VANILLA);
    builder.from(Registry.ITEM.get(Key.key("example:ash_block")));
    builder.to(ItemType.LIGHT_GRAY_CONCRETE_POWDER);
});
event.register(builder -> {
    builder.awarenessLevel(ClientView.AwarenessLevel.RESOURCE_PACK);
    builder.from(Registry.ITEM.get(Key.key("example:ash_block")));
    builder.to(ItemType.BARRIER);
});

When registering a mapping using to(ItemType)) like above, Fiddle will attempt to display the original item as accurately as possible. For example, the item name, rarity and durability will all be kept at the same value as the original.

You can also register function mappings:

event.register(builder -> {
    builder.everyAwarenessLevel();
    builder.from(Items.CRAFTING_TABLE);
    builder.to(handle -> {
        // Add a lore line to each crafting table
        var newLore = Component.text("This is a very important block for beginners!")
            .decoration(TextDecoration.ITALIC, false).color(TextColor.color(5526612));
        var lore = handle.getImmutable().lore();
        if (lore == null) {
            lore = new ArrayList<>();
        }
        lore.add(newLore);
        handle.getMutable().lore(lore);
    });
});

While the handle for blocks only supported getOriginal(), getImmutable() and set(..), the handle for items also supports:

  • getMutable() - Returns a mutable ItemStack that you can modify directly
    (Do not make changes directly to the ItemStack returned by getImmutable())
  • setMutable(ItemStack) - The same as set(..), except with the guarantee that this ItemStack is allowed to be mutated further (in other words, you don't need it anymore and it is not reference anywhere else)

Unlike blocks, function mappings for items are not slower than simple mappings, but you should still make sure the code in your mappings runs very fast.

Text components

Sometimes, you also want to change the text that clients see.

Typically, you want to add some translations, such as names for your blocks and items. These are normally defined in a resource pack, but you likely also want them to be visible to players without the resource pack. That is why Fiddle allows you to register server-side translations that display correctly for each type of client.

To do so, you can register with the FiddleEvents.SERVER_SIDE_TRANSLATION event and add translations:

context.getLifecycleManager().registerEventHandler(
    FiddleEvents.SERVER_SIDE_TRANSLATION,
    event -> {
        // Register translations here
    }
);

Translations are added with the key they would normally have in the resource pack. For example, you can set the name of an item like this:

event.register("item.example.ash", "Ash");

For items, you can also get the key dynamically like this:

event.register(Registry.ITEM.get(Key.key("example:ash")).translationKey(), "Ash");

You can register translations for specific languages. When doing so, you can specify whether you want this translation to also act as a fallback for other languages from the same language group (typically a region), or as a fallback for all languages (when those languages don't have a specific translation):

event.register(
    "item.example.ash",
    "灰", // The text
    "ja_jp", // The language
    ServerSideTranslations.FallbackScope.LANGUAGE_GROUP // Acts as fallback for other languages that start with "ja_"
);

You can also override existing translations. For example, if you want to add a bookshelf for each woodtype, you can rename the vanilla bookshelf:

event.register("block.minecraft.bookshelf", "Oak Bookshelf");

If you want to change any text component sent to the client, you can also register mappings with the FiddleEvents.COMPONENT_MAPPING event:

context.getLifecycleManager().registerEventHandler(
    FiddleEvents.COMPONENT_MAPPING,
    event -> {
        event.register(builder -> {
            builder.from(ComponentTarget.TRANSLATABLE);
            builder.to(handle -> {
                if (handle.getOriginal() instanceof TranslatableComponent translatable) {
                    if (translatable.key().equals("item.example.ash")) {
                        handle.set(handle.getImmutable().decorate(TextDecoration.BOLD));
                    }
                }
            });
        });
    }
);

Be aware that a lot of components are sent to the client, so make sure the code in your mappings runs very fast.

Miscellaneous

Customizing enum names

Fiddle will add all custom blocks and items to the Bukkit Material enum automatically. For example, example:ash_block will get a corresponding Material.FIDDLE_EXAMPLE_ASH_BLOCK. The format used is always FIDDLE_<namespace>_<key>. This format is chosen to prevent collisions with (future) vanilla enums.

You can choose a different enum name too. To do so, register with the FiddleEvents.MATERIAL_ENUM_NAME event and add mappings:

context.getLifecycleManager().registerEventHandler(
    FiddleEvents.MATERIAL_ENUM_NAME,
    event -> {
        event.register(handle -> {
            var key = handle.getSourceValue().getLeft();
            if (key.equals(NamespacedKey.fromString("example:ash"))) {
                handle.set("ASHES_TO_DUST"); // Instead of FIDDLE_EXAMPLE_ASH
            }
        });
    }
);

Please only do this if you know what you are doing! Changing the enum name for blocks and items that existed on the server previously may corrupt existing plugin data.

Clone this wiki locally