-
Notifications
You must be signed in to change notification settings - Fork 0
Detailed guide
This is a detailed guide of what you can do with Fiddle, step by step.
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)
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 anet.minecraft.world.level.Blockinstance
Typically, this means you do the following:
- Make a
factoryNMScall if you want to use a specificnet.minecraft.world.level.Blocktype, such asStairBlock,AnvilBlock,FlowerPotBlocketc. - Make a
propertiesNMScall 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);
});
}
);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
}
);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);
}
}
);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.
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: Tominecraft:light_gray_concrete_powder
(looks the most similar) -
RESOURCE_PACK: Tominecraft: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 receivesminecraft:ash_blockunchanged
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 originalBlockData -
getImmutable()- Returns the currentBlockDatato 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 aBlockMappingFunctionContextinstance, 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.
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 mutableItemStackthat you can modify directly
(Do not make changes directly to theItemStackreturned bygetImmutable()) -
setMutable(ItemStack)- The same asset(..), except with the guarantee that thisItemStackis 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.
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.
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.