MultiLoader 1.21+ · Part 11

Custom Armour (MultiLoader 1.21+)

INTERMEDIATE MULTILOADER 1.21-1.21.1 22 min read · Jul 20, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

In 1.21, ArmorMaterial became a data-driven record registered in the game's built-in registry rather than an enum. Each material declares per-slot defence values, enchantability, an equip sound, a repair ingredient, and one or more texture layers used for the 3D equipped rendering. The inventory icons are separate item textures, just like any other flat item.

NOTE
Complete the Creating Items tutorial before continuing. Armour items are registered in the same ItemRegistry.

Armour Material

Create a new class called ArmourMaterialRegistry in your common registry package. Declare a RegistrationProvider<ArmorMaterial> and register your material:

java
public class ArmourMaterialRegistry {
public static final RegistrationProvider<ArmorMaterial> ARMOUR_MATERIALS =
RegistrationProvider.get(Registries.ARMOR_MATERIAL, Constants.MOD_ID);
public static final RegistryObject<ArmorMaterial, ArmorMaterial> EXAMPLE =
ARMOUR_MATERIALS.register("example", () -> new ArmorMaterial(
Util.make(new EnumMap<>(ArmorItem.Type.class), map -> {
map.put(ArmorItem.Type.HELMET, 2);
map.put(ArmorItem.Type.CHESTPLATE, 6);
map.put(ArmorItem.Type.LEGGINGS, 5);
map.put(ArmorItem.Type.BOOTS, 2);
}),
15,
SoundEvents.ARMOR_EQUIP_IRON,
() -> Ingredient.of(ItemRegistry.IRON_STICK.get()),
List.of(new ArmorMaterial.Layer(
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "example"))),
0.0f,
0.0f
));
public static void init() {}
}

The seven arguments to ArmorMaterial are:

  • defence map: damage reduction points per slot (iron totals 15: 2+6+5+2).
  • enchantmentValue: enchantability (iron is 9, gold is 25, diamond is 10).
  • equipSound: the Holder<SoundEvent> played on equip.
  • repairIngredient: item accepted by an anvil to repair the armour.
  • layers: list of texture layers shown on the player model when equipped.
  • toughness: extra damage reduction for heavy armour (diamond: 2, netherite: 4, iron: 0).
  • knockbackResistance: fraction of knockback absorbed (netherite: 0.1, others: 0).

Call ArmourMaterialRegistry.init() from CommonClass.init() before the item registry so the material is registered before the armour items that reference it:

java
public class CommonClass {
public static void init() {
ArmourMaterialRegistry.init();
ItemRegistry.init();
BlockRegistry.init();
CreativeTabRegistry.init();
}
}

Registering Armour Items

ArmorItem takes a Holder<ArmorMaterial>, not the material object directly. Retrieve it with Holder.direct() inside the item supplier, which is safe because the supplier is only evaluated after the material registry has been populated. Also, let's set the max stack size for the ItemStack to 1 by calling.stacksTo(1) on getItemProperties():

java
public static final RegistryObject<Item, ArmorItem> EXAMPLE_HELMET =
ITEMS.register("example_helmet", () -> new ArmorItem(
Holder.direct(ArmourMaterialRegistry.EXAMPLE.get()),
ArmorItem.Type.HELMET,
getItemProperties().stacksTo(1)));
public static final RegistryObject<Item, ArmorItem> EXAMPLE_CHESTPLATE =
ITEMS.register("example_chestplate", () -> new ArmorItem(
Holder.direct(ArmourMaterialRegistry.EXAMPLE.get()),
ArmorItem.Type.CHESTPLATE,
getItemProperties().stacksTo(1)));
public static final RegistryObject<Item, ArmorItem> EXAMPLE_LEGGINGS =
ITEMS.register("example_leggings", () -> new ArmorItem(
Holder.direct(ArmourMaterialRegistry.EXAMPLE.get()),
ArmorItem.Type.LEGGINGS,
getItemProperties().stacksTo(1)));
public static final RegistryObject<Item, ArmorItem> EXAMPLE_BOOTS =
ITEMS.register("example_boots", () -> new ArmorItem(
Holder.direct(ArmourMaterialRegistry.EXAMPLE.get()),
ArmorItem.Type.BOOTS,
getItemProperties().stacksTo(1)));
TIP
Holder.direct() creates an unkeyed holder, which works at runtime but means the material cannot be referenced by data packs or other mods by its registry key. For full interoperability, retrieve the keyed holder after registration using BuiltInRegistries.ARMOR_MATERIAL.getHolder(ResourceKey...) and store it in astatic Holder<ArmorMaterial> field initialised in a RegisterEvent or equivalent post-registration hook.

Armour Textures

The 3D equipped model uses two texture files named after the layer identifier you declared. For the ArmorMaterial.Layer created with ResourceLocation.fromNamespaceAndPath(MOD_ID, "example"), Minecraft looks for:

  • assets/examplemod/textures/models/armor/example_layer_1.png: worn on the helmet, chestplate, and boots model.
  • assets/examplemod/textures/models/armor/example_layer_2.png: worn on the leggings model.

Both files follow the vanilla armour UV layout. The easiest way to create them is to copy a vanilla armour texture from the Minecraft assets as a starting point and recolour or redraw from there.

TIP
The vanilla armour texture template is 64×32 pixels. Layer 1 covers the helmet, chestplate, and boots regions; layer 2 covers only the leggings region. Every standard armour piece fits within those coordinates.

Creative Tab and Language File

Add all four pieces to your creative tab and add translation keys to en_us.json:

java
output.accept(ItemRegistry.EXAMPLE_HELMET.get());
output.accept(ItemRegistry.EXAMPLE_CHESTPLATE.get());
output.accept(ItemRegistry.EXAMPLE_LEGGINGS.get());
output.accept(ItemRegistry.EXAMPLE_BOOTS.get());
json
"item.examplemod.example_helmet": "Example Helmet",
"item.examplemod.example_chestplate": "Example Chestplate",
"item.examplemod.example_leggings": "Example Leggings",
"item.examplemod.example_boots": "Example Boots"

Datagen

Armour items use the standard flat 2D inventory model, so each piece needs a basicItem call in ExampleItemModelProvider:

java
basicItem(ItemRegistry.EXAMPLE_HELMET.get());
basicItem(ItemRegistry.EXAMPLE_CHESTPLATE.get());
basicItem(ItemRegistry.EXAMPLE_LEGGINGS.get());
basicItem(ItemRegistry.EXAMPLE_BOOTS.get());

Create a 16×16 PNG inventory icon for each piece under src/main/resources/assets/examplemod/textures/item/ and run NeoForge Data to generate the model files.

Testing In-Game

Launch the client and equip each piece from your creative tab. Confirm:

  • The inventory icon for each slot shows your item texture.
  • The equipped 3D model on the player uses your layer textures.
  • The armour bar (chestplate icon at the top of the hotbar) fills according to your defence values.
  • Placing the pieces in an anvil with Iron Sticks repairs their durability.

If the equipped model appears as the default leather armour texture or shows missing textures, double-check the layer texture file names. The number suffix (_layer_1, _layer_2) must be present and the folder must be textures/models/armor/ not textures/armor/.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Block Entities (MultiLoader 1.21+)

Attach persistent data and ticking logic to a block using BlockEntityType, implement saveAdditional and loadAdditional for NBT persistence, and sync state to clients with getUpdatePacket.

Continue →