Repository navigation
Developers Worldgen Info 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
IServerRegistryinterface 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), …).
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.
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
ConfiguredFeaturepairs aFeaturewith aFeatureConfiguration, registered inworldgen/configured_feature. Both methods take the configuration'sClass<? 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); }
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
PlacedFeatureleads to aConfiguredFeature, and it is that one's configuration you collect from; a block state provider is held directly, not in aHolder: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.
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.
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 asdetails(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.EMPTYfor 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)).
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.
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
typeof the rule in its JSON. It is aResourceLocationon 1.21.1 and older. - The factory runs once per dimension scan with an
ISurfaceRuleHandler.Context: the dimension'sRandomState(for its noises, and on 26.3 its density samplers) and aHolderLookup.Provider, through which a definition that references registry entries can be decoded (context.lookup().createSerializationContext(JsonOps.INSTANCE); on 1.20.1RegistryOps.create(JsonOps.INSTANCE, context.lookup())). Returningnull, 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 |
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.
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.fastSee Tooltips → Enum values for the key shape and the fallback behaviour.
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.
Users
Loot Info
Worldgen Info
Developers
Loot Info
Examples
Worldgen Info