Creating custom content with Minecraft Resource Packs
Welcome to this post where I’ll be sharing my experience creating custom content for Minecraft using resource packs and plugins. This is an area that I find really interesting, and I hope that I can share some useful tips and tricks that I’ve learned along the way.
A bit of background
Section titled “A bit of background”Before we dive into the details, let me give you a bit of background as to how we got here. A good while ago, I wanted to create a custom minecraft server such as MCC Island; however, I didn’t particularly want to make a PvP only server rather than a more general minigame server with custom content. I had very little experience with Minecraft plugins at this time and so I started by learning how to make basic custom assets using resource packs. I found a thread on the spigot forums that explained how to create custom content using resource packs and will be very helpful later on. Right so that’s case closed right? Not quite. Their methods whilst helpful weren’t exactly what I needed.
The problem with the existing methods
Section titled “The problem with the existing methods”The methods described in the spigot thread were great for creating custom items and blocks, but they were hard to understand at times, back when theirs was written custom model data could only store floats and now strings, so let’s say you had multiple plugins with custom content, how would the plugins know whose custom content was whose? Well there kind of wasn’t a good solution to this, but that doesn’t matter anymore because
now custom model data can store strings, flags, and colours for some reason, so we can just use the modern string format to store the plugin name such as inbetween:tnt_launcher—technically you don’t need to format the string like this but it just makes it easier to read and understand. And also they presumibly didn’t have access to the new Display Entities which are really useful for creating custom content, so they had to use armor stands which just makes less sense than using display entities.
Custom Items
Section titled “Custom Items”Now a lot of what xenondevs wrote in their post is still relevant, and I’m not saying theirs doesn’t work for this, but I just think that this new method I use is much easier to understand and implement, so let’s get into it.
To create a custom item, you first need to create a resource pack. If you don’t know how to do this, I recommend checking out this tutorial on the Minecraft wiki. Once you have your resource pack set up, you can start creating your custom items.
To do this we’ll need to override a vanilla item, so for this example, we’ll override the Popped Chorus Fruit (minecraft:popped_chorus_fruit) since it doesn’t have any default use—Test Block also works well and we’ll introduce it later but that’s a more advanced clause. To override the item, we need to create a new model for it in our resource pack. This is done by creating a new JSON file in the assets/minecraft/items directory of your resource pack. The name of the file should be the same as the item you want to override, so in this case, it would be popped_chorus_fruit.json. The contents of the file should look something like this:
{ "model": { "type": "minecraft:select", "property": "minecraft:custom_model_data", "cases": [ { "when": "inbetween:tnt_launcher", "model": { "type": "minecraft:model", "model": "inbetween:item/tnt_launcher" } } ], "fallback": { "type": "minecraft:model", "model": "minecraft:item/popped_chorus_fruit" } }}In this case if you just grab a Popped Chorus Fruit you’ll notice that it looks exactly the same as the vanilla item, so we need to specify a custom_model_data component on it. You can do this using the /give command like so:
/give @p minecraft:popped_chorus_fruit[minecraft:custom_model_data={strings:["inbetween:tnt_launcher"]}]Making the item do something
Section titled “Making the item do something”Now that we have our custom item, we need to make it do something. This is where plugins come in (or datapacks but then you probably want to use some sort of armor and scoreboard stats but I’m not going to cover that here).
For this guide, we’ll be assuming that you’re running a PaperMC (or forks) server.
To make our custom item do something, we need to listen for when a player uses it. This is done by listening for the PlayerInteractEvent event in your plugin. Here’s an example of how to do this:
import org.bukkit.Bukkit;import org.bukkit.event.EventHandler;import org.bukkit.event.Listener;import org.bukkit.event.block.Action;import org.bukkit.event.player.PlayerInteractEvent;import org.bukkit.inventory.ItemStack;
public class TNTLauncherItem implements Listener { static ItemStack tntLauncherItem = Items.TNT_LAUNCHER; // This is a reference to an ItemStack from a custom item registry, you can implement this however you like or hardcode it right here
public TNTLauncherItem(JavaPlugin plugin) { Bukkit.getServer().getPluginManager().registerEvents(this, plugin); }
@EventHandler public void onPlayerInteract(PlayerInteractEvent event) { ItemStack item = event.getItem(); if (item != null && item.isSimilar(tntLauncherItem) && (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK)) { event.getPlayer().sendRichMessage("<green>Custom item used!</green>"); // Just an example of something you could do when the item is used, you can replace this with whatever you want such as spawning a primed TNT entity and launching it in the direction the player is looking } }}And that’s it! Now when you right-click with the item in your hand, it will send a message to the player. I’m not going to go into detail on how to implement the actual functionality of the item as this is just an example, but if you want to implement a TNT launcher, I actually have a implementation of this in my Inbetween plugin; however, this doesn’t follow the best practices for implementing custom items and is just a quick and dirty implementation to get the job done.
Custom blocks
Section titled “Custom blocks”Custom blocks are a bit more complicated than custom items, but why do we need to override an existing block when we can just cover up a block with a display entity. Now we have to pick out the block we want to use as the base for our custom block, this block needs to meet a couple requirements (atleast to play well):
- It needs to be almost a full block so that it can be covered up by a display entity without any weird clipping issues, but not so small that it causes hitbox issues.
- It needs to not modify the player’s movement in any way, so soul sand, and honey/slime blocks are out of the question.
- It needs to not have any interesting interactions with the player.
- It must be a block entity so that it doesn’t cause clipping issues with the display entity.
With these requirements in mind, the best block is probably a kind of chest—I personally use ender chests due to mining speed—but wait a minute, chests violate rules 1 and 3, so why do they work? Well, the chest size is close enough to a full block that we don’t really care, and players probably wont notice.
As for the second issue, we can just stop the chest from opening by listening for the InventoryOpenEvent event and cancelling it if the bock is one of our custom blocks.
Placing the block
Section titled “Placing the block”Now we need to palce the block in the world, and we can do this by listening for the PlayerInteractEvent event, if you’re wondering why we’re listening for player interactions instead of block placements, it’s because our custom item isn’t placeable and so we need to do that ourself. Here’s an example of how to do this:
static Map<String, CustomBlock> customBlockMap = Map.of( "lfs:lucky_block", new LuckyBlockBlock());
@EventHandlerpublic void onBlockPlace(PlayerInteractEvent event) { // We're using PlayerInteractEvent as the block doesn't actually get placed for test_block // This is using test blocks as it's a extract taken from another plugin where I chose to use test blocks for custom items if (!event.getAction().equals(Action.RIGHT_CLICK_BLOCK) || event.getItem() == null || !event.getItem().getType().equals(Material.TEST_BLOCK)) { return; }
// Make sure to check if the item has the custom model data component before we try to access it. if (!event.getItem().hasItemMeta() || !event.getItem().getItemMeta().hasCustomModelDataComponent()) { return; }
// This is short for custom model data, just written weirdly. String cmd = event.getItem().getItemMeta().getCustomModelDataComponent().getStrings().getFirst();
boolean success = true;
assert event.getClickedBlock() != null; Location placeLocation = event.getClickedBlock().getRelative(event.getBlockFace()).getLocation();
// Center the display entity in the block so that it appears as an actual block. Location displayLocation = placeLocation.add(0.5, 0.5, 0.5); if (!displayLocation.getNearbyEntitiesByType(ItemDisplay.class, 0.1).isEmpty()) { // Don't place two blocks in one. success = false; }
// Check if there are any entities (players, mobs, etc.) at the placement location // We exclude ItemDisplay entities (which are custom blocks themselves) and item entities (which are drops) Collection<Entity> nearbyEntities = placeLocation.getWorld().getNearbyEntities(placeLocation.toCenterLocation(), 0.5, 0.5, 0.5, entity -> !(entity instanceof ItemDisplay) && !(entity instanceof org.bukkit.entity.Item)); if (!nearbyEntities.isEmpty()) { success = false; }
CustomBlock customBlock = customBlockMap.get(cmd); if (customBlock == null) return;
// CustomBlock::canPlace is a method we'll cover later when we talk about custom blocks themselves not just this part of the handler success = customBlock.canPlace(placeLocation, event, success);
if (!success) { return; }
placeLocation.getBlock().setType(Material.ENDER_CHEST); ItemDisplay display = placeLocation.getWorld().spawn(displayLocation, ItemDisplay.class); display.setItemStack(event.getItem().asQuantity(1)); display.setTransformation( new Transformation( display.getTransformation().getTranslation(), display.getTransformation().getLeftRotation(), // We scale a tiny bit to prevent z-fighting issues with the block we're covering up, chests do have some edges that are full. display.getTransformation().getScale().add(0.001F, 0.001F, 0.001F), display.getTransformation().getRightRotation() ) );
// Swing the player's hand to make it look like they're placing the block, this is purely cosmetic but it adds a nice touch. event.getPlayer().swingMainHand();
// Yet again, we'll cover this later. customBlock.onPlace(placeLocation, event);
// Ensure that we only consume one item from the player's hand, and that we don't consume the item if the player is in creative mode. if (event.getPlayer().getGameMode().equals(GameMode.CREATIVE)) return; event.getItem().setAmount(event.getItem().getAmount() - 1);}This is pretty much all you need to do to place the block, but it still just looks like the item floating in the air. To make it look like an actual block, we obviously need to make a model for it, the model is pretty simple, and minecraft includes a base model for this kind of thing. Below is an example of a model for a heart core block.
{ "parent": "minecraft:block/cube_all", "textures": { "all": "lfs:block/lucky_block" }}The CustomBlock class
Section titled “The CustomBlock class”Now in the earlier code we saw quite a few references to a CustomBlock class. The code for this class is pretty simple, and is shown below:
public class CustomBlock { public boolean hasDrops() { return true; }
/** * Called to check if a block can be placed at a location * @param loc The location where the block is being placed * @param event The PlayerInteractEvent that triggered the placement * @param verdict What CustomBlockHandler has determined so far */ public boolean canPlace(Location loc, PlayerInteractEvent event, boolean verdict) { return verdict; // We don't modify the verdict by default }
public void onPlace(Location loc, PlayerInteractEvent event) { Bukkit.getLogger().info("CustomBlock placed at " + loc.toString() + " by " + event.getPlayer().getName()); }
public void onBreak(Location loc, @Nullable BlockBreakEvent event) { Bukkit.getLogger().info("CustomBlock broken at " + loc.toString() + " by " + (event == null ? "Unknown" : event.getPlayer().getName())); }}Example custom block implementation
Section titled “Example custom block implementation”Now with this class, you can create custom blocks by extending it and overriding the methods to add custom functionality. For example, if we wanted to create a custom block that explodes when broken, we could do something like this:
public class ExplosiveBlock extends CustomBlock { @Override public void onBreak(Location loc, @Nullable BlockBreakEvent event) { loc.getWorld().createExplosion(loc, 4F); // Create an explosion with a power of 4 at the location of the block }}And that’s pretty much all there is to it! Just make sure to register your custom block in the customBlockMap and you’re good to go. The key is the custom model data string that you use for the block item, and the value is an instance of your custom block class.
Finalized CustomBlockHandler class
Section titled “Finalized CustomBlockHandler class”Now that we have the CustomBlock class, we can create a CustomBlockHandler class that will handle all the events related to custom blocks. I would advise writing your own implementation so you really understand how it works, but if you just want a functional implementation, below is the full code for a CustomBlockHandler class that handles placing and breaking custom blocks, as well as preventing them from being interacted with in any way.
import org.bukkit.Bukkit;import org.bukkit.GameMode;import org.bukkit.Location;import org.bukkit.Material;import org.bukkit.block.Block;import org.bukkit.entity.Entity;import org.bukkit.entity.ItemDisplay;import org.bukkit.entity.Player;import org.bukkit.event.EventHandler;import org.bukkit.event.Listener;import org.bukkit.event.block.Action;import org.bukkit.event.block.BlockBreakEvent;import org.bukkit.event.inventory.InventoryOpenEvent;import org.bukkit.event.inventory.InventoryType;import org.bukkit.event.player.PlayerInteractEvent;import org.bukkit.util.Transformation;
import java.util.Collection;import java.util.Map;
public class CustomBlockHandler implements Listener { static Map<String, CustomBlock> customBlockMap = Map.of( "lfs:explosive", new ExplosiveBlock() );
public CustomBlockHandler(JavaPlugin plugin) { Bukkit.getServer().getPluginManager().registerEvents(this, plugin); }
public static void remove(Location loc) { Location displayLocation = loc.add(0.5, 0.5, 0.5); if (!loc.getBlock().getType().equals(Material.ENDER_CHEST) || displayLocation.getNearbyEntitiesByType(ItemDisplay.class, 0.1).isEmpty()) { return; }
ItemDisplay display = displayLocation.getNearbyEntitiesByType(ItemDisplay.class, 0.1).stream().findFirst().orElseThrow(); display.remove(); Location breakLocation = loc; CustomBlock customBlock = customBlockMap.get(display.getItemStack().getItemMeta().getCustomModelDataComponent().getStrings().getFirst());
if (customBlock != null) { if (customBlock.hasDrops()) { breakLocation.getWorld().dropItemNaturally(breakLocation.add(0.5, 0.5, 0.5), display.getItemStack()); } customBlock.onBreak(breakLocation, null); } else { Bukkit.getLogger().info("No custom block found for broken block at location: " + breakLocation); } }
@EventHandler public void onBlockPlace(PlayerInteractEvent event) { // We're using PlayerInteractEvent as the block doesn't actually get placed for test_block if (!event.getAction().equals(Action.RIGHT_CLICK_BLOCK) || event.getItem() == null || !event.getItem().getType().equals(Material.TEST_BLOCK)) { return; }
// Make sure to check if the item has the custom model data component before we try to access it. if (!event.getItem().hasItemMeta() || !event.getItem().getItemMeta().hasCustomModelDataComponent()) { return; }
// This is short for custom model data, just written weirdly. String cmd = event.getItem().getItemMeta().getCustomModelDataComponent().getStrings().getFirst();
boolean success = true;
assert event.getClickedBlock() != null; Location placeLocation = event.getClickedBlock().getRelative(event.getBlockFace()).getLocation();
// Center the display entity in the block so that it appears as an actual block. Location displayLocation = placeLocation.add(0.5, 0.5, 0.5); if (!displayLocation.getNearbyEntitiesByType(ItemDisplay.class, 0.1).isEmpty()) { // Don't place two blocks in one. success = false; }
// Check if there are any entities (players, mobs, etc.) at the placement location // We exclude ItemDisplay entities (which are custom blocks themselves) and item entities (which are drops) Collection<Entity> nearbyEntities = placeLocation.getWorld().getNearbyEntities(placeLocation.toCenterLocation(), 0.5, 0.5, 0.5, entity -> !(entity instanceof ItemDisplay) && !(entity instanceof org.bukkit.entity.Item)); if (!nearbyEntities.isEmpty()) { success = false; }
CustomBlock customBlock = customBlockMap.get(cmd); if (customBlock == null) return; success = customBlock.canPlace(placeLocation, event, success);
if (!success) { return; }
placeLocation.getBlock().setType(Material.ENDER_CHEST); ItemDisplay display = placeLocation.getWorld().spawn(displayLocation, ItemDisplay.class); display.setItemStack(event.getItem().asQuantity(1)); display.setTransformation( new Transformation( display.getTransformation().getTranslation(), display.getTransformation().getLeftRotation(), // We scale a tiny bit to prevent z-fighting issues with the block we're covering up, chests do have some edges that are full. display.getTransformation().getScale().add(0.001F, 0.001F, 0.001F), display.getTransformation().getRightRotation() ) );
// Swing the player's hand to make it look like they're placing the block, this is purely cosmetic but it adds a nice touch. event.getPlayer().swingMainHand();
customBlock.onPlace(placeLocation, event);
// Ensure that we only consume one item from the player's hand, and that we don't consume the item if the player is in creative mode. if (event.getPlayer().getGameMode().equals(GameMode.CREATIVE)) return; event.getItem().setAmount(event.getItem().getAmount() - 1); }
@EventHandler public void onBlockBreak(BlockBreakEvent event) { // Basically #remove but stops drops Location displayLocation = event.getBlock().getLocation().add(0.5, 0.5, 0.5); if (!event.getBlock().getType().equals(Material.ENDER_CHEST) || displayLocation.getNearbyEntitiesByType(ItemDisplay.class, 0.1).isEmpty()) { return; }
ItemDisplay display = displayLocation.getNearbyEntitiesByType(ItemDisplay.class, 0.1).stream().findFirst().orElseThrow(); display.remove(); Location breakLocation = event.getBlock().getLocation(); CustomBlock customBlock = customBlockMap.get(display.getItemStack().getItemMeta().getCustomModelDataComponent().getStrings().getFirst()); event.setDropItems(false); // Prevent default drops
if (customBlock != null) { if (customBlock.hasDrops()) { breakLocation.getWorld().dropItemNaturally(breakLocation.add(0.5, 0.5, 0.5), display.getItemStack()); } customBlock.onBreak(breakLocation, event); } else { Bukkit.getLogger().info("No custom block found for broken block at location: " + breakLocation); } }
@EventHandler public void onInventoryOpen(InventoryOpenEvent event) { Player openingPlayer = (Player) event.getPlayer(); if (event.getInventory().getType().equals(InventoryType.ENDER_CHEST)) { // Possible custom block interaction
Block lookingBlock = openingPlayer.getTargetBlockExact(5); if (lookingBlock != null && lookingBlock.getType().equals(Material.ENDER_CHEST)) { Location displayLocation = lookingBlock.getLocation().add(0.5, 0.5, 0.5); if (!displayLocation.getNearbyEntitiesByType(ItemDisplay.class, 0.1).isEmpty()) { // It's a custom block, cancel the inventory open event.setCancelled(true); } } } }}Custom GUIs
Section titled “Custom GUIs”Custom GUIs are considerably trickier than custom items and blocks. It seems the method detailed in the post by xenondevs seems to be broken. This is certainly possible but I personally haven’t found much success with this method, and it instead seems much easier to just use the PaperMC Dialog API which allows you to create custom GUIs with much more ease than the method described in the spigot thread. The Dialog API is pretty straightforward to use, and I would recommend checking out the documentation for it if you want to create custom GUIs for your plugin.
Conclusion
Section titled “Conclusion”And that’s pretty much all there is to it! With these methods, you can create custom items, blocks, and even GUIs for your Minecraft plugin using resource packs and the PaperMC API. The key is to use custom model data to differentiate between different custom items and blocks, and to use display entities to create the illusion of custom blocks in the world. With a bit of creativity, the possibilities are well.. practically endless (with the exclusion of truly custom registry entries but that’s a moot point). I hope this post has been helpful, and if you have any questions or suggestions, feel free to message me on discord (@lilacember) and maybe I’ll even update this post with some of the suggestions!