Skip to content

Developers Worldgen Info Registration Reference

Yanny edited this page Oct 3, 2026 · 7 revisions

Registration Reference

Everything below is registered from registerServer(IServerRegistry registry) unless stated otherwise. Read Tooltips first — it explains TooltipBuilder and the difference between exact-class category lookup and superclass-walking value lookup. The AWI-specific registrations on this page are exact-class (except features and surface rule handlers, which are keyed differently): each of them maps one-to-one to an entry in a vanilla worldgen registry, so register every concrete class you ship; registering a shared abstract base does nothing.

Availability differs by Minecraft version. The API follows vanilla: a registration method only exists where the vanilla concept exists. When in doubt, check the IServerRegistry interface in the jar you compile against.

Each registration has a matching getter on IServerUtils, which is how you recurse into a nested value from inside your own tooltip (utils.getPlacementModifierTooltip(utils, modifier), utils.getBlockPredicateTooltip(utils, predicate), …).

Tooltips

They share one shape — the key of the type plus a BiFunction<IServerUtils, T, TooltipBuilder>:

registry.registerPlacementModifierTooltip(MyPlacement.class, MyTooltips::getMyPlacementTooltip);
public static TooltipBuilder getMyPlacementTooltip(IServerUtils utils, MyPlacement placement) {
    return TooltipBuilder.array((b) -> {
        b.add(utils.getValueTooltip(utils, placement.count()).build(MyLang.Branch.COUNT));
        b.add(utils.getBlockPredicateTooltip(utils, placement.predicate()));
    }, MyLang.PlacementModifier.MY_PLACEMENT);
}

The key is the Class for every category except features — see Features below.

Method Describes
registerFeatureTooltip a Feature (up to 26.2 a FeatureConfiguration) — see Features
registerPlacementModifierTooltip a PlacementModifier
registerHeightProviderTooltip a HeightProvider
registerBlockPredicateTooltip a BlockPredicate
registerBlockStateProviderTooltip a BlockStateProvider
registerRuleTestTooltip a RuleTest
registerStructureProcessorTooltip a StructureProcessor
registerFeatureSizeTooltip a FeatureSize
registerTrunkPlacerTooltip a TrunkPlacer
registerFoliagePlacerTooltip a FoliagePlacer
registerRootPlacerTooltip a RootPlacer
registerTreeDecoratorTooltip a TreeDecorator

An IntProvider or FloatProvider has no tooltip registration: inside a tooltip it is rendered as the number its converter produces.

Returning TooltipBuilder.empty() is a valid answer for a type that carries nothing worth showing — vanilla's BiomeFilter does exactly that. Leaving a type unregistered instead makes it fall through to the JSON dump.

Features

A feature and its configuration are one object: each feature type is its own record holding its settings, and the worldgen/feature registry holds the instances.

Because of that, the two feature registrations are the only ones not keyed by a Class. A feature has no dedicated type object, and what the worldgen/feature_type registry holds is its MapCodec, so that is the key:

registry.registerFeatureTooltip(MyFeature.CODEC, MyTooltips::getMyFeatureTooltip);
registry.registerFeatureBlockCollector(MyFeature.CODEC, MyCollectors::collectMyFeature);
public static TooltipBuilder getMyFeatureTooltip(IServerUtils utils, MyFeature feature) {
    return TooltipBuilder.array((b) -> {
        b.add(utils.getValueTooltip(utils, feature.block()).build(MyLang.Branch.BLOCK));
        b.add(utils.getBlockPredicateTooltip(utils, feature.canReplace()));
    }, MyLang.Feature.MY_FEATURE);
}

Pass the same MapCodec constant you registered in worldgen/feature_type; AWI looks a feature up by feature.codec().

Up to 26.2: a feature and its configuration are separate — a ConfiguredFeature pairs a Feature with a FeatureConfiguration, registered in worldgen/configured_feature. Both methods take the configuration's Class<? extends FeatureConfiguration>, and the tooltip and collector receive the configuration:

registry.registerFeatureTooltip(MyConfiguration.class, MyTooltips::getMyConfigurationTooltip);
registry.registerFeatureBlockCollector(MyConfiguration.class, MyCollectors::collectMyConfiguration);
public static TooltipBuilder getMyConfigurationTooltip(IServerUtils utils, MyConfiguration config) {
    return TooltipBuilder.array((b) -> {
        b.add(utils.getValueTooltip(utils, config.block()).build(MyLang.Branch.BLOCK));
        b.add(utils.getBlockPredicateTooltip(utils, config.canReplace()));
    }, MyLang.Feature.MY_FEATURE);
}

Block collectors

A tooltip describes a feature; a collector says which blocks it can place. That is what fills the block slots under the feature and what makes the feature findable when a player looks that block up — a feature with no collected blocks has no children left, and a node with no children is dropped from the tree, so it is not displayed at all.

registry.registerFeatureBlockCollector(MyFeature.CODEC, (utils, feature) ->
        List.of(Either.left(feature.block()), Either.right(MyTags.MY_BLOCKS)));

Up to 26.2: keyed by the configuration class, and the collector receives the configuration:

registry.registerFeatureBlockCollector(MyConfiguration.class, (utils, config) ->
        List.of(Either.left(config.block()), Either.right(MyTags.MY_BLOCKS)));
Method Returns For
registerFeatureBlockCollector List<Either<Block, TagKey<Block>>> a Feature (up to 26.2 a FeatureConfiguration)
registerStateProviderBlockCollector List<Block> a BlockStateProvider
registerRootPlacerBlockCollector List<Block> a RootPlacer
registerTreeDecoratorBlockCollector List<Block> a TreeDecorator

Only the feature family can return a tag. A tag survives to the client as a tag and becomes one cycling slot named after it, instead of one slot per member — return Either.right(tag) rather than expanding it yourself. The other three families cannot express a tag, so they return plain blocks.

Inside a collector, reach nested values through utils.collectBlocks(utils, …), which is overloaded for all four families. A feature that contains another feature must be descended into through the same mechanism, otherwise the nested feature's blocks are lost.

A nested feature is a Holder<PlacedFeature>, so reach the feature through placed.value().feature().value() — check isBound() on both hops first. A feature that holds a block state provider holds a Holder<BlockStateProvider> too; unwrap it with value() before calling utils.collectBlocks. Tooltips need no unwrapping: a Holder passed to getValueTooltip renders its value.

registry.registerFeatureBlockCollector(MyFeature.CODEC, (utils, feature) -> {
    List<Either<Block, TagKey<Block>>> blocks = new ArrayList<>();
    Holder<PlacedFeature> placed = feature.inner();

    if (placed.isBound() && placed.value().feature().isBound()) {
        blocks.addAll(utils.collectBlocks(utils, placed.value().feature().value()));
    }

    if (feature.stateProvider().isBound()) {
        utils.collectBlocks(utils, feature.stateProvider().value()).forEach((block) -> blocks.add(Either.left(block)));
    }

    return blocks;
});

Up to 26.2: the nested PlacedFeature leads to a ConfiguredFeature, and it is that one's configuration you collect from; a block state provider is held directly, not in a Holder:

registry.registerFeatureBlockCollector(MyConfiguration.class, (utils, config) -> {
    List<Either<Block, TagKey<Block>>> blocks = new ArrayList<>();
    Holder<PlacedFeature> placed = config.inner();

    if (placed.isBound() && placed.value().feature().isBound()) {
        blocks.addAll(utils.collectBlocks(utils, placed.value().feature().value().config()));
    }

    utils.collectBlocks(utils, config.stateProvider()).forEach((block) -> blocks.add(Either.left(block)));
    return blocks;
});

Register a collector even when your feature places no blocks of its own. The descent into nested features happens inside the collector, so a feature left unregistered contributes nothing and cuts off everything nested below it.

Placement summary propagators

The three lines at the top of a placed feature — Attempts Per Chunk, Chance, Height — are not read off the tooltips. They are computed by walking the feature's placement modifiers and merging what each contributes. A modded placement modifier that changes the count, the chance or the height should say so, or the feature is summarized as if the modifier were not there.

registerPlacementPropagator

The main extensibility point. Returns a PlacementContribution, which fills at most one of the three axes. Each axis is a NumberExpr (see Tooltips → Numbers), so the client shows its range, most likely value, chart and — with F3 + H — formula without any further work:

registry.registerPlacementPropagator(MyRarityFilter.class, (utils, modifier, ctx) ->
        PlacementContribution.ofChance(NumberExpr.constant(1.0 / modifier.chance()), List.of()));

registry.registerPlacementPropagator(MyCountPlacement.class, (utils, modifier, ctx) -> {
    List<TooltipNode> conditions = new ArrayList<>();
    NumberExpr count = utils.convertIntProvider(utils, modifier.count(), conditions);

    return PlacementContribution.ofCount(count, conditions);
});

registry.registerPlacementPropagator(MyHeightPlacement.class, (utils, modifier, ctx) -> {
    List<TooltipNode> conditions = new ArrayList<>();
    NumberExpr height = utils.convertHeightProvider(utils, modifier.height(), ctx, conditions);

    return PlacementContribution.ofHeight(HeightSpan.of(height, conditions));
});

PlacementContribution, HeightSpan and ColumnContext live in com.yanny.awi.plugin.server.summary.

The conditions list travels with the number: a NumberExpr.cond branch refers to a condition by its index in it, and the converters append the condition tooltips they produce. Pass a fresh list into the conversion and hand the same list on with the result; a number without conditions takes List.of().

  • The chance is a fraction (0.25), rendered as a percent.
  • A count that depends on the world (noise, terrain) cannot be computed: report PlacementContribution.ofCount(NumberExpr.opaque("<modifier type id>"), details), with the modifier's own tooltip as details (utils.getPlacementModifierTooltip(utils, modifier).build()) — it is shown under the ? — rather than inventing a range.
  • HeightSpan.relativeTo(heightmap) is the answer for a height that is relative to terrain and has no absolute Y.
  • Use PlacementContribution.EMPTY for a modifier that affects none of the three (a filter, a scan, a spread).

Merging follows vanilla semantics: the first count and the first height win, while chances compound (p1 × p2). The ColumnContext third argument carries the dimension's minY and height, and resolves a VerticalAnchor without a world (ctx.resolveY(anchor)).

registerIntProvider / registerFloatProvider / registerHeightProvider

Only needed for an IntProvider, FloatProvider or HeightProvider of your own. Registering it converts the provider into a NumberExpr wherever a number is computed from it — in a placement summary, whichever modifier hands it over. An int or float provider is also converted as a value inside a feature's tooltip (a radius, a spread); a height provider's tooltip comes from registerHeightProviderTooltip instead. An unregistered provider shows as ? (<provider type>).

registry.registerIntProvider(MyIntProvider.class, (utils, provider, conditions) ->
        NumberExpr.uniformInt(provider.min(), provider.max()));

registry.registerHeightProvider(MyHeightProvider.class, (utils, provider, ctx, conditions) ->
        NumberExpr.uniformInt(ctx.resolveY(provider.min()), ctx.resolveY(provider.max())));

The int and float converters are a com.yanny.aci.api.NumberConverter — (utils, provider, conditions); the height converter is a HeightConverter from com.yanny.awi.plugin.server.summary, which also takes the ColumnContext. The conditions list is the one described above.

registerIntProvider and registerFloatProvider belong to the shared registry, so ALI has them too. A provider wrapping another one converts the inner one through utils.convertIntProvider / convertFloatProvider / convertHeightProvider, so a modded provider nested inside it still reaches its own converter. Describe the distribution only as far as it is known: NumberExpr.range(min, max) gives the bounds without a most likely value. A distribution the built-in factories cannot express is a number function of your own.

Surface rule handlers

A biome's base terrain is found by running the dimension's material rule (a surface rule up to 26.2) over sample columns and recording what it places. A rule type whose output is not a plain function of the position — noise-driven layers, a density function — only comes out as scattered fragments that way. A handler takes over one rule type: AWI hands it the rule's definition and it answers with the blocks the rule places. Vanilla's own minecraft:bandlands (the badlands terracotta bands) and minecraft:ore_vein (26.3 only) are handled like this.

registry.registerSurfaceRuleHandler(MyRules.MY_RULE, (context) -> new MyRuleHandler(context.randomState()));
  • The key is the id the rule type is registered under — the type of the rule in its JSON. It is a ResourceLocation on 1.21.1 and older.
  • The factory runs once per dimension scan with an ISurfaceRuleHandler.Context: the dimension's RandomState (for its noises, and on 26.3 its density samplers) and a HolderLookup.Provider, through which a definition that references registry entries can be decoded (context.lookup().createSerializationContext(JsonOps.INSTANCE); on 1.20.1 RegistryOps.create(JsonOps.INSTANCE, context.lookup())). Returning null, or throwing, leaves the rule measured as if no handler were registered.
public class MyRuleHandler implements ISurfaceRuleHandler {
    @Override
    public List<BlockInfo> expand(JsonObject definition, GhostObservation ghost) {
        NumberInterval range = NumberInterval.closed(ghost.absoluteY().first(), ghost.absoluteY().last());

        return List.of(new BlockInfo(Blocks.STONE, BlockInfo.StorageType.ABSOLUTE, List.of(range), 0,
                ghost.water(), ghost.placement(), List.of()));
    }
}

expand receives the rule node exactly as it appears in the serialized rule, and a GhostObservation: the absolute Ys at which the rule was reached for this biome, and whether it was reached underwater or on land and on a floor or a ceiling. It returns one BlockInfo per block and way of placing it; throwing leaves just this rule out of the biome. expand is called for every biome the rule applies to, so cache anything expensive in the handler.

alwaysPlaces() — override it to return false when the rule can pass a position on to the next rule instead of placing a block there (an ore vein places nothing where its density is not positive). A rule that always places is replaced by a placeholder block during the scan, and the observation says where the placeholder landed. A placeholder for a rule that does not always place would hide every rule after it, so AWI only checks whether the rule can apply to the biome and passes the whole height of the dimension, underwater and on land, floor and ceiling (ANY). The handler then has to work out itself at which Ys the rule places anything.

BlockInfo component Meaning
block the block
storageType RELATIVE — ranges are depths below the surface; ABSOLUTE — a fixed Y range; LAYERED — recurring bands at absolute Ys
ranges the Y or depth ranges, in the units of storageType
layerShift for LAYERED, how far the bands move up or down from place to place; 0 otherwise
water UNDERWATER, DRY or ANY
placement FLOOR, CEILING or ANY
heights for RELATIVE, the absolute Ys the block is confined to; empty when it occurs at any height

Value types

registerValueTooltip is the shared, superclass-aware tier — it is on the core registry, not AWI-specific:

registry.registerValueTooltip(MyPayload.class, (utils, value) -> TooltipBuilder.array((b) -> {
    b.add(utils.getValueTooltip(utils, value.block()).build(MyLang.Value.BLOCK));
    b.add(utils.getValueTooltip(utils, value.radius()).build(MyLang.Value.RADIUS));
}));

Use it for your own records and data holders that appear inside another tooltip. Enums, collections, optionals, numbers, Holders, tags, block states and the like are already handled — do not register those. See Tooltips.

registerEnumTranslation

Optional. An enum is already displayed through a translation key derived from its class name; this only replaces the namespace and the derived part with ones you choose:

registry.registerEnumTranslation(MyMode.class, "mymod", "my_mode");   // mymod.enum.my_mode.fast

See Tooltips → Enum values for the key shape and the fallback behaviour.

Caches

registerCacheCleaner is on the core registry too: a Runnable that empties a cache your tooltips or collectors keep, run once data collection has finished. See Plugin Lifecycle → Caches for data collection.

Clone this wiki locally