MultiLoader 1.21+ · Part 20

Ore Generation (MultiLoader 1.21+)

ADVANCED MULTILOADER 1.21-1.21.1 25 min read · Sep 21, 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+)

Ore generation is driven by the world generation data pack system: a ConfiguredFeature describes what generates and where it can replace, and a PlacedFeature describes frequency and vertical placement. NeoForge datagen writes these as JSON; Fabric applies them at runtime through the Fabric API biome modifications API.

NOTE
Complete the Creating Blocks tutorial before continuing. We generate ore placement for the New Dirt block.

Feature Resource Keys

Declare the resource keys in common so both loaders and the datagen system can reference them without duplicating strings. Create a ExampleWorldGen class in your common project in aworld package:

java
public class ExampleWorldGen {
public static final ResourceKey<ConfiguredFeature<?, ?>> NEW_DIRT_ORE_CONFIGURED =
ResourceKey.create(Registries.CONFIGURED_FEATURE,
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "new_dirt_ore"));
public static final ResourceKey<PlacedFeature> NEW_DIRT_ORE_PLACED =
ResourceKey.create(Registries.PLACED_FEATURE,
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "new_dirt_ore"));
}

NeoForge Datagen Setup

NeoForge generates world gen JSON through a DatapackBuiltinEntriesProvider backed by a RegistrySetBuilder. Create a helper class ExampleWorldGenProvider in your NeoForge data package and register it in gatherData:

java
// In gatherData:
generator.addProvider(true,
new DatapackBuiltinEntriesProvider(output, registries,
new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, ExampleWorldGenProvider::configuredFeatures)
.add(Registries.PLACED_FEATURE, ExampleWorldGenProvider::placedFeatures)
.add(NeoForgeRegistries.Keys.BIOME_MODIFIERS, ExampleWorldGenProvider::biomeModifiers),
Set.of(Constants.MOD_ID)));

Configured Feature

The configured feature defines the ore block, vein size, and which blocks it can replace. Add a configuredFeatures static method to ExampleWorldGenProvider:

java
public static void configuredFeatures(BootstrapContext<ConfiguredFeature<?, ?>> context) {
RuleTest stoneReplaceables = new TagMatchTest(BlockTags.STONE_ORE_REPLACEABLES);
RuleTest deepslateReplaceables = new TagMatchTest(BlockTags.DEEPSLATE_ORE_REPLACEABLES);
context.register(ExampleWorldGen.NEW_DIRT_ORE_CONFIGURED,
new ConfiguredFeature<>(Feature.ORE, new OreConfiguration(
List.of(
OreConfiguration.target(stoneReplaceables,
BlockRegistry.NEW_DIRT.get().defaultBlockState()),
OreConfiguration.target(deepslateReplaceables,
BlockRegistry.NEW_DIRT.get().defaultBlockState())
),
9 // vein size
)));
}

TagMatchTest wraps a block tag into a RuleTest, which is what OreConfiguration.target expects. Using BlockTags.STONE_ORE_REPLACEABLES and BlockTags.DEEPSLATE_ORE_REPLACEABLES makes the ore generate in both stone and deepslate layers, matching the behaviour of vanilla iron ore. The vein size of 9 is comparable to iron. Adjust both values to taste.

Placed Feature

The placed feature references the configured feature and layers on placement modifiers: how many veins per chunk, the height range, and any surface/terrain filters:

java
public static void placedFeatures(BootstrapContext<PlacedFeature> context) {
HolderGetter<ConfiguredFeature<?, ?>> configured =
context.lookup(Registries.CONFIGURED_FEATURE);
context.register(ExampleWorldGen.NEW_DIRT_ORE_PLACED,
new PlacedFeature(
configured.getOrThrow(ExampleWorldGen.NEW_DIRT_ORE_CONFIGURED),
List.of(
CountPlacement.of(4), // veins per chunk
InSquarePlacement.spread(), // scatter across the chunk column
HeightRangePlacement.triangle(
VerticalAnchor.absolute(-80),
VerticalAnchor.absolute(80)),
BiomeFilter.biome() // skip chunks whose biome doesn't match
)
));
}

The vanilla OrePlacements helper has private methods in 1.21, so the four modifiers are listed explicitly here. CountPlacement sets the number of vein attempts per chunk. InSquarePlacement scatters each attempt randomly within the 16×16 column. HeightRangePlacement.triangle picks a Y level with a triangular distribution that peaks at the midpoint of the range - matching how vanilla ores like iron concentrate at certain Y levels. BiomeFilter discards attempts in biomes that haven't been configured to include this feature.

Biome Modifier

A NeoForge biome modifier injects the placed feature into matching biomes. The built-in BiomeModifiers.AddFeaturesBiomeModifier adds a placed feature to all biomes matching a tag:

java
public static void biomeModifiers(BootstrapContext<BiomeModifier> context) {
HolderGetter<PlacedFeature> placed = context.lookup(Registries.PLACED_FEATURE);
HolderGetter<Biome> biomes = context.lookup(Registries.BIOME);
context.register(
ResourceKey.create(NeoForgeRegistries.Keys.BIOME_MODIFIERS,
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "add_new_dirt_ore")),
new BiomeModifiers.AddFeaturesBiomeModifier(
biomes.getOrThrow(BiomeTags.IS_OVERWORLD),
HolderSet.direct(placed.getOrThrow(ExampleWorldGen.NEW_DIRT_ORE_PLACED)),
GenerationStep.Decoration.UNDERGROUND_ORES
));
}

Fabric World Generation

On Fabric, biome modification is done at runtime in the mod initialiser using the Fabric API. The placed feature JSON generated by NeoForge datagen is shared, so only the registration call differs:

java
// In ExampleModFabric.onInitialize()
BiomeModifications.addFeature(
BiomeSelectors.foundInOverworld(),
GenerationStep.Decoration.UNDERGROUND_ORES,
ExampleWorldGen.NEW_DIRT_ORE_PLACED);
TIP
Fabric's BiomeModifications reads the placed feature from the world gen registry at world load, so the JSON files produced by NeoForge datagen are picked up automatically on Fabric too. You do not need separate Fabric datagen for the feature files themselves.

Testing

Run datagen first to produce the JSON files, then launch either client. Create a new world (the ore placement only applies to newly generated chunks). Use /locate biome minecraft:plains to teleport to an open area and dig down, or enable spectator mode and fly underground. You should find New Dirt veins scattered through stone and deepslate layers.

Use /place feature examplemod:new_dirt_ore at any underground position to force a single vein to generate immediately for quick verification.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Introduction to Mixins (MultiLoader 1.21+)

Write your first @Inject mixin against a vanilla class, use CallbackInfoReturnable to override return values, expose private fields with @Shadow, and understand the common pitfalls that cause startup crashes.

Continue →