Skip to content

Environment attribute API for adding custom attribute layers#5201

Open
FoxSamu wants to merge 4 commits intoFabricMC:26.1from
FoxSamu:26.1
Open

Environment attribute API for adding custom attribute layers#5201
FoxSamu wants to merge 4 commits intoFabricMC:26.1from
FoxSamu:26.1

Conversation

@FoxSamu
Copy link
Contributor

@FoxSamu FoxSamu commented Feb 9, 2026

This might be a little far-fetched, but with Minecraft's introduction of environment attributes comes an intricate system of attribute layers that interpolate and modify the environment attributes throughout different environments. This PR suggests a new API that lets mod developers add custom layers to this system.

To give some background, Minecraft creates an EnvironmentAttributeSystem for each Level, both ClientLevel and ServerLevel get one, and the setup is pretty much the same for both. The EnvironmentAttributeSystem is built with a builder (EnvironmentAttributeSystem.Builder), which is fed through EnvironmentAttributeSystem.addDefaultLayers(Builder, Level) to register Minecraft's attribute layers that deliver pretty much all logic:

  • First it adds a layer that provides dimension attribute overrides;
  • then comes a layer that provides biome attribute overrides;
  • then a layer that provides timeline attribute interpolationsl
  • and finally it adds a layer that provides some hardcoded overrides for weather, if the level has that.

That code looks something like this:

private static void addDefaultLayers(final EnvironmentAttributeSystem.Builder builder, final Level level) {
    RegistryAccess registries = level.registryAccess();
    BiomeManager biomeManager = level.getBiomeManager();
    ClockManager clockManager = level.clockManager();

    // Dimension layer
    addDimensionLayer(builder, level.dimensionType());

    // Biome layer
    addBiomeLayer(builder, registries.lookupOrThrow(Registries.BIOME), biomeManager);

    // Timeline layer
    level.dimensionType().timelines().forEach(timeline -> builder.addTimelineLayer(timeline, clockManager));

    // Weather layer
    if (level.canHaveWeather()) {
        WeatherAttributes.addBuiltinLayers(builder, WeatherAttributes.WeatherAccess.from(level));
    }
}

This system is very modular, and could be easily extended by mods using a simple mixin (presumably Mojang did this by design). I thought it might be nice to have this accessible through the Fabric API, hence I'm submitting this PR that adds just that. I created a new API, fabric-environment-attributes-v0, which supplies several events that allow inserting attribute layers before, after or in between Minecraft's layers.

So say I want to add a layer that sits between the dimension layer and biome layer that makes clouds red, I could do the following:

EnvironmentAttributeEvents
        .insertLayersEvent(AttributeLayerPosition.BETWEEN_DIMENSION_AND_BIOMES)
        .register((systemBuilder, level) -> {
            systemBuilder.addConstantLayer(EnvironmentAttributes.CLOUD_COLOR, base -> 0xFFFF0000);
        });

This code overrides the cloud colors defined by dimensions, but biomes, timelines and weather can in turn override the red cloud color I provided.

To clarify, this is not the same as DimensionEvents.MODIFY_ATTRIBUTES. The dimension event allows modifying the attributes of a dimension type (which should still remain a proper API since it's much less convoluted and has a different purpose). Meanwhile, attribute layers define the actual logic that Minecraft uses to interpolate and override attributes. This would, for example, allow for creating custom weather that modifies environment attributes temporarily through a custom weather layer.

@FoxSamu
Copy link
Contributor Author

FoxSamu commented Feb 9, 2026

BTW, I couldn't get the client test to run because some other API's test failed and I did not see a way to run the client test only for this specific API. However, I did see the test working while I was looking through the client tests (the sky was purple, as modded by the client test).

@Juuxel Juuxel added enhancement New feature or request new module Pull requests that introduce new modules labels Feb 9, 2026
@PepperCode1
Copy link
Member

The idea is good, but I think the design should be changed to be more flexible and in-line with other Fabric API code. I am imagining a static registry that works much like ResourceLoader's registerReloadListener and addListenerOrdering, but for attribute layers instead of reload listeners. The current InsertLayers interface would be turned into a top-level AttributeLayer interface and there would be a class with fields for the Identifiers of vanilla layers, like VanillaAttributeLayerIds.DIMENSION = new Identifier("dimension");. The main benefits with this design over the current one is that users don't need to subscribe to an extra event and can instead use their ModInitializer and that using Identifiers instead of an enum allows ordering between mod-added layers too. There are toposort utilities in the base module that can be used, and I imagine that the toposort would happen when addDefaultLayers is called and its results will be cached, if possible, and reused unless more layers were added since the last toposort.

@FoxSamu
Copy link
Contributor Author

FoxSamu commented Feb 9, 2026

The idea is good, but I think the design should be changed to be more flexible and in-line with other Fabric API code. I am imagining a static registry that works much like ResourceLoader's registerReloadListener and addListenerOrdering, but for attribute layers instead of reload listeners. The current InsertLayers interface would be turned into a top-level AttributeLayer interface and there would be a class with fields for the Identifiers of vanilla layers, like VanillaAttributeLayerIds.DIMENSION = new Identifier("dimension");. The main benefits with this design over the current one is that users don't need to subscribe to an extra event and can instead use their ModInitializer and that using Identifiers instead of an enum allows ordering between mod-added layers too. There are toposort utilities in the base module that can be used, and I imagine that the toposort would happen when addDefaultLayers is called and its results will be cached, if possible, and reused unless more layers were added since the last toposort.

I noticed the toposort abilities, I though of somehow making a phased Event that has vanilla's logic registered to it in phases. This seemed a bit strange to me though, since this would require completely overwriting the addDefaultLayers method. Right now it is just a mixin hook at several places in the same method.

I'll see if I can make it somehow work in the way that you described.

@PepperCode1
Copy link
Member

ResourceLoader has the advantage that it can operate on the List<PreparableReloadListener> extracted from a vanilla code path, but no such list or even layer type exists here in vanilla. Perhaps it's possible to make such a list using marker objects and slice it after sorting so the existing non-intrusive injectors can be used instead of an Overwrite. I would like some feedback from others.

@cputnam-a11y
Copy link
Contributor

I believe I agree with marker objects as well.

@FoxSamu
Copy link
Contributor Author

FoxSamu commented Feb 9, 2026

Yeah I implemented it with marker objects now in a local project (since working directly in the fabric api project is pretty slow and tough to test). It seems to do well, but I think it is a bit confusing that now you have to call two or three functions to register layers in the correct order:

AttributeLayerRegistry.registerLayerProvider(ID, ModEnvironmentAttributes::setupLayers);
AttributeLayerRegistry.addLayerOrdering(ID, AttributeLayerProvider.WEATHER);

I did make it so that, unless a defined layer dependency prevents it, the sorter prefers to put vanilla layers before modded layers. This is probably the most preferred use. Nevertheless, I find it a bit confusing that it's method calls.

Will push the code to the PR tomorrow.

@PepperCode1
Copy link
Member

These changes look good but the terminology seems to be inconsistent. I think "layer" should exclusively refer to EnvironmentAttributeLayer; right now it is also used instead of "phase" in many places in the API javadoc, method names, and registry implementation. I think that a "phase" should be one of the four places in the vanilla method that adds layers, or an AttributeLayerProvider instance, or the ID of one of these; a phase adds zero or more layers to the builder. I also think that all phase fields and identifiers should be singular.

@FoxSamu
Copy link
Contributor Author

FoxSamu commented Feb 11, 2026

I think that a "phase" should be one of the four places in the vanilla method that adds layers, or an AttributeLayerProvider instance, or the ID of one of these; a phase adds zero or more layers to the builder.

I would then rather stick with the name "provider" - it provides layers to the system - and then interpret the vanilla phases as providers too (despite those just being markers).

I also doubted if I should name it EnvironmentAttributeLayerProvider, it is more obviously related to evironment attributes, but it's very long.

I also think that all phase fields and identifiers should be singular.

I think plural is better since a single vanilla provider provides layers that interpolate between a multitude of things. One layer handles dimensions, another does biomes, and so on and so forth. But it's not a big deal to me to change it to singular, it just feels a bit off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request new module Pull requests that introduce new modules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants