Skip to main content

Overview

Modifiers are the core building blocks of Lithostitched’s worldgen customization system. They provide a powerful interface for programmatically modifying Minecraft’s world generation at runtime, allowing you to add, remove, or modify worldgen features, structures, surface rules, and more.
Modifiers are loaded from data packs and applied when the server starts, before world generation begins.

The Modifier Interface

At its core, every modifier implements the Modifier interface:
/home/daytona/workspace/source/src/common/main/java/dev/worldgen/lithostitched/worldgen/modifier/Modifier.java:17-42
public interface Modifier {
    // Dispatch codec for serialization
    Codec<Modifier> CODEC = Codec.lazyInitialized(() -> {
        var modifierRegistry = BuiltInRegistries.REGISTRY.getOptional(LithostitchedRegistryKeys.MODIFIER_TYPE.identifier());
        if (modifierRegistry.isEmpty()) throw new NullPointerException("Worldgen modifier registry does not exist yet!");
        return ((Registry<MapCodec<? extends Modifier>>) modifierRegistry.get()).byNameCodec();
    }).dispatch(Modifier::codec, Function.identity());

    // Priority presets
    MapCodec<Integer> PRIORITY_DEFAULT = Codec.INT.optionalFieldOf("priority", 1000);
    MapCodec<Integer> PRIORITY_REMOVE = Codec.INT.optionalFieldOf("priority", 2000);

    default void applyModifier(RegistryAccess registryAccess) {
        this.applyModifier();
    }

    void applyModifier();

    int priority();

    MapCodec<? extends Modifier> codec();

    default boolean internal$modifiesFabricFeatures() {
        return false;
    }
}

Key Methods

void applyModifier();
void applyModifier(RegistryAccess registryAccess);
  • applyModifier() - The main method that executes your worldgen modifications
  • priority() - Determines the order in which modifiers are applied (lower = earlier)
  • codec() - Returns the MapCodec used for JSON serialization/deserialization

Priority System

Modifiers are applied in order based on their priority value. This is crucial for ensuring modifications happen in the correct sequence.
{
  "type": "lithostitched:add_surface_rule",
  "priority": 500,
  "levels": ["minecraft:overworld"],
  "surface_rule": { ... }
}

Priority Presets

  • PRIORITY_DEFAULT - 1000 (standard modifications)
  • PRIORITY_REMOVE - 2000 (removal operations should happen last)
Removal modifiers should typically have higher priority values so they execute after additions. Otherwise, you might remove something that another modifier was supposed to add.

How Modifiers Are Loaded

Modifiers are loaded through Minecraft’s data pack system and applied by the ModifierManager:
/home/daytona/workspace/source/src/common/main/java/dev/worldgen/lithostitched/worldgen/modifier/ModifierManager.java:21-44
public static void applyModifiers(MinecraftServer server) {
    boolean fabricFeaturesModified = false;
    RegistryAccess registries = server.registryAccess();
    HolderLookup.RegistryLookup<Modifier> modifiers = registries.lookupOrThrow(LithostitchedRegistryKeys.WORLDGEN_MODIFIER);

    for (Holder.Reference<Modifier> reference : sortByPriority(modifiers.listElements())) {
        Lithostitched.debug("Applying modifier with id: {}", reference.key().identifier());
        reference.value().applyModifier(registries);

        if (reference.value().internal$modifiesFabricFeatures()) {
            fabricFeaturesModified = true;
        }
    }

    if (fabricFeaturesModified) {
        for (LevelStem dimension : Lithostitched.registry(registries, Registries.LEVEL_STEM).stream().toList()) {
            var accessor = ((ChunkGeneratorAccessor)dimension.generator());
            BiomeSource source = accessor.getBiomeSource();
            accessor.setFeaturesPerStep(Suppliers.memoize(() ->
                FeatureSorter.buildFeaturesPerStep(List.copyOf(source.possibleBiomes()), biome -> accessor.getGetter().apply(biome).features(), true)
            ));
        }
    }
}

Loading Process

1

Server Startup

Modifiers are loaded when the Minecraft server starts, triggered by mixins into the server initialization
2

Registry Lookup

The ModifierManager queries the WORLDGEN_MODIFIER registry for all registered modifiers
3

Priority Sorting

All modifiers are sorted by their priority value (ascending order)
4

Sequential Application

Each modifier’s applyModifier() method is called in order
5

Feature Recompilation

If any modifiers affect Fabric features, feature placement is recompiled

Built-in Modifier Types

Lithostitched provides many built-in modifier types for common operations:

add_surface_rule

Adds surface rules to dimension noise routers

add_template_pool_elements

Adds new structure pieces to template pools

add_structure_set_entries

Adds structures to structure sets

remove_structure_set_entries

Removes structures from structure sets

wrap_density_function

Wraps or modifies density functions

wrap_noise_router

Modifies noise router configuration

set_pool_aliases

Creates aliases for template pools

stack_feature

Stacks multiple features together

Example: Creating a Custom Modifier

Here’s a complete example of a custom modifier that removes structures:
/home/daytona/workspace/source/src/common/main/java/dev/worldgen/lithostitched/worldgen/modifier/RemoveStructureSetEntriesModifier.java:23-45
public record RemoveStructureSetEntriesModifier(int priority, HolderSet<StructureSet> structureSets, List<Holder<Structure>> entries) implements Modifier {
    public static final MapCodec<RemoveStructureSetEntriesModifier> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
        PRIORITY_REMOVE.forGetter(RemoveStructureSetEntriesModifier::priority),
        registrySet(Registries.STRUCTURE_SET, "structure_sets").forGetter(RemoveStructureSetEntriesModifier::structureSets),
        Structure.CODEC.listOf().fieldOf("structures").forGetter(RemoveStructureSetEntriesModifier::entries)
    ).apply(instance, RemoveStructureSetEntriesModifier::new));

    @Override
    public void applyModifier() {
        this.structureSets.stream().map(Holder::value).forEach(this::applyModifier);
    }

    private void applyModifier(StructureSet structureSet) {
        StructureSetAccessor structureSetAccessor = ((StructureSetAccessor)(Object)structureSet);
        List<StructureSet.StructureSelectionEntry> structureSelectionEntries = new ArrayList<>(structureSet.structures());
        structureSetAccessor.setStructures(structureSelectionEntries.stream().filter(setEntry -> !entries.contains(setEntry.structure())).collect(Collectors.toList()));
    }

    @Override
    public MapCodec<? extends Modifier> codec() {
        return CODEC;
    }
}
This modifier uses PRIORITY_REMOVE (2000) to ensure it runs after other modifiers that might add structures.

Best Practices

  • Use lower priorities (500-900) for foundational changes
  • Use default priority (1000) for standard modifications
  • Use higher priorities (1500-2000) for removals and overrides
If modifiers depend on each other, test different priority values to ensure they apply in the correct sequence.
Enable log_debug_messages in the Lithostitched config to see the order in which modifiers are applied.
When using applyModifier(RegistryAccess registryAccess), ensure the registries you need are available at modification time.

Registry System

Learn how modifiers are registered

Data-Driven Config

Create modifier JSON files

Build docs developers (and LLMs) love