diff --git a/fabric-environment-attributes-v1/build.gradle b/fabric-environment-attributes-v1/build.gradle
new file mode 100644
index 0000000000..8bfe314df9
--- /dev/null
+++ b/fabric-environment-attributes-v1/build.gradle
@@ -0,0 +1,11 @@
+version = getSubprojectVersion(project)
+
+moduleDependencies(project, [
+ 'fabric-api-base',
+ 'fabric-lifecycle-events-v1'
+])
+
+testDependencies(project, [
+ ':fabric-resource-loader-v1',
+ ':fabric-client-gametest-api-v1'
+])
diff --git a/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/api/environment/attribute/v1/AttributeLayerProvider.java b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/api/environment/attribute/v1/AttributeLayerProvider.java
new file mode 100644
index 0000000000..422ab0f768
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/api/environment/attribute/v1/AttributeLayerProvider.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.environment.attribute.v1;
+
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.attribute.EnvironmentAttributeLayer;
+import net.minecraft.world.attribute.EnvironmentAttributeSystem;
+import net.minecraft.world.level.Level;
+
+/**
+ * Provides {@link EnvironmentAttributeLayer}s to an {@link EnvironmentAttributeSystem}. You may register custom
+ * {@link AttributeLayerProvider} implementations using {@link AttributeLayerRegistry#registerLayerProvider}.
+ *
+ *
+ * Attribute layers can be ordered relative to vanilla's layers or other modded layers using
+ * {@link AttributeLayerRegistry#addLayerOrdering}. The order defines which layers override which other layers: layers
+ * that come first in the ordering are overriden by layers that come later in the ordering. For example, in vanilla,
+ * biome layers come after dimension layers, since biome-local attributes override dimension-global attributes.
+ *
+ *
+ *
+ * Minecraft adds layers in four phases: dimension-global attributes, then biome-local attributes, then
+ * timeline-interpolated attributes, and finally some hardcoded weather attributes. Each of these phases, as well modded
+ * layer providers, are associated with an identifier that can be sorted against.
+ *
+ */
+public interface AttributeLayerProvider {
+ /**
+ * Identifier associated to vanilla's dimension attribute layers.
+ */
+ Identifier DIMENSION = Identifier.withDefaultNamespace("dimensions");
+
+ /**
+ * Identifier associated to vanilla's biome attribute layers.
+ */
+ Identifier BIOMES = Identifier.withDefaultNamespace("biomes");
+
+ /**
+ * Identifier associated to vanilla's timeline attribute layers.
+ */
+ Identifier TIMELINES = Identifier.withDefaultNamespace("timelines");
+
+ /**
+ * Identifier associated to vanilla's weather attribute layers.
+ */
+ Identifier WEATHER = Identifier.withDefaultNamespace("weather");
+
+ /**
+ * The identifier associated to the first vanilla phase. Currently, that is {@link #DIMENSION}.
+ * This constant exists purely for compatibility: if Minecraft ever adds another layer before its dimension phase,
+ * then this constant is updated.
+ */
+ Identifier FIRST_VANILLA_PHASE = DIMENSION;
+
+ /**
+ * The identifier associated to the last vanilla phase. Currently, that is {@link #WEATHER}.
+ * This constant exists purely for compatibility: if Minecraft ever adds another layer after its weather phase,
+ * then this constant is updated.
+ */
+ Identifier LAST_VANILLA_PHASE = WEATHER;
+
+ /**
+ * Called to add attribute layers to an {@link EnvironmentAttributeSystem.Builder} for the given {@link Level}.
+ * This is called both on the client and on the server for every {@link Level} that is created.
+ *
+ * @param systemBuilder The {@link EnvironmentAttributeSystem.Builder} to add layers to.
+ * @param level The {@link Level} that the environment attribute system is being created for.
+ */
+ void addAttributeLayers(EnvironmentAttributeSystem.Builder systemBuilder, Level level);
+}
diff --git a/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/api/environment/attribute/v1/AttributeLayerRegistry.java b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/api/environment/attribute/v1/AttributeLayerRegistry.java
new file mode 100644
index 0000000000..bda6521402
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/api/environment/attribute/v1/AttributeLayerRegistry.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.environment.attribute.v1;
+
+import org.jspecify.annotations.NullMarked;
+
+import net.minecraft.resources.Identifier;
+
+import net.fabricmc.fabric.impl.environment.attribute.AttributeLayerRegistryImpl;
+
+@NullMarked
+public class AttributeLayerRegistry {
+ /**
+ * Register a {@link AttributeLayerProvider}. If a layer with the given identifier already exists, an exception
+ * is thrown.
+ *
+ * @param id The identifier of the layer provider. This can be ordered against by other layer providers.
+ * @param layer The layer provider to register.
+ */
+ public static void registerLayerProvider(Identifier id, AttributeLayerProvider layer) {
+ AttributeLayerRegistryImpl.registerLayerProvider(id, layer);
+ }
+
+ /**
+ * Declares that the layer provider with the first identifier should activate before the layer provider with the
+ * second identifier. Unless this causes a cyclic dependency, the two layer providers are guaranteed to activate in
+ * said order. You may use this to order your layer provider against vanilla phases using any of the constants in
+ * {@link AttributeLayerProvider}. If both layer identifiers are the same, then an exception is thrown.
+ *
+ * @param firstLayer The ID of the layer that should activate earlier.
+ * @param secondLayer The ID of the layer that should activate later.
+ */
+ public static void addLayerOrdering(Identifier firstLayer, Identifier secondLayer) {
+ AttributeLayerRegistryImpl.addLayerOrdering(firstLayer, secondLayer);
+ }
+}
diff --git a/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/impl/environment/attribute/AttributeLayerRegistryImpl.java b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/impl/environment/attribute/AttributeLayerRegistryImpl.java
new file mode 100644
index 0000000000..0433e2107c
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/impl/environment/attribute/AttributeLayerRegistryImpl.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.environment.attribute;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.attribute.EnvironmentAttributeSystem;
+import net.minecraft.world.level.Level;
+
+import net.fabricmc.fabric.api.environment.attribute.v1.AttributeLayerProvider;
+import net.fabricmc.fabric.impl.base.toposort.NodeSorting;
+import net.fabricmc.fabric.impl.base.toposort.SortableNode;
+
+public class AttributeLayerRegistryImpl {
+ // Markes for each vanilla phase. Used to ensure vanilla ordering remains the same.
+ private static final Map MARKERS = Map.copyOf(Stream.of(VanillaLayerMarker.values())
+ .collect(Collectors.toMap(marker -> marker.id, marker -> marker)));
+
+ private static final Map LAYER_MAP = new HashMap<>();
+ private static final Set DEPENDENCIES = new HashSet<>();
+
+ // Lock used to ensure thread safety.
+ private static final Object LOCK = new Object();
+
+ // As long as this is true, we skip sorting and inserting layers all together.
+ // Becomes false once a modded layer is registered.
+ private static volatile boolean hasOnlyVanillaLayers;
+
+ // As long as this is true, the ordering in the fields below is valid.
+ // Becomes false once a modded layer is registered or once a depencency order is added.
+ private static volatile boolean orderValid;
+
+ // Layers that should go before vanilla layers.
+ private static final List FIRST_PHASES = new ArrayList<>();
+
+ // Layers that should go in between or after vanilla layers.
+ private static final Map> AFTER_VANILLA_PHASES = new EnumMap<>(VanillaLayerMarker.class);
+
+ static {
+ // Initialize sorted phase lists
+ for (VanillaLayerMarker layer : VanillaLayerMarker.values()) {
+ AFTER_VANILLA_PHASES.put(layer, new ArrayList<>());
+ }
+
+ // Register vanilla ordering
+ registerLayerProvider(AttributeLayerProvider.DIMENSION, VanillaLayerMarker.DIMENSION);
+ registerLayerProvider(AttributeLayerProvider.BIOMES, VanillaLayerMarker.BIOMES);
+ registerLayerProvider(AttributeLayerProvider.TIMELINES, VanillaLayerMarker.TIMELINES);
+ registerLayerProvider(AttributeLayerProvider.WEATHER, VanillaLayerMarker.WEATHER);
+
+ addLayerOrdering(AttributeLayerProvider.DIMENSION, AttributeLayerProvider.BIOMES);
+ addLayerOrdering(AttributeLayerProvider.BIOMES, AttributeLayerProvider.TIMELINES);
+ addLayerOrdering(AttributeLayerProvider.TIMELINES, AttributeLayerProvider.WEATHER);
+
+ // Validate cache
+ hasOnlyVanillaLayers = true; // Set to true here because registerLayerProvider used above sets it to false
+ orderValid = true; // Vanilla layers are not included in sorted phase lists
+ }
+
+ public static void registerLayerProvider(Identifier id, AttributeLayerProvider layer) {
+ Objects.requireNonNull(id, "The layer identifier should not be null.");
+ Objects.requireNonNull(layer, "The layer should not be null.");
+
+ if (LAYER_MAP.containsKey(id)) {
+ throw new IllegalArgumentException("Layer with ID %s was already registered.".formatted(id));
+ }
+
+ synchronized (LOCK) {
+ LAYER_MAP.put(id, layer);
+ orderValid = false;
+ hasOnlyVanillaLayers = false;
+ }
+ }
+
+ public static void addLayerOrdering(Identifier firstLayer, Identifier secondLayer) {
+ Objects.requireNonNull(firstLayer, "The first layer identifier should not be null.");
+ Objects.requireNonNull(secondLayer, "The second layer identifier should not be null.");
+
+ if (firstLayer.equals(secondLayer)) {
+ throw new IllegalArgumentException("Tried to add a layer that depends on itself.");
+ }
+
+ synchronized (LOCK) {
+ if (DEPENDENCIES.add(new Dependency(firstLayer, secondLayer))) {
+ // Adding a dependency only affects order if both IDs are associated with registered layers.
+ // Dependencies with missing registrations are simply ignored during sorting.
+
+ if (LAYER_MAP.containsKey(firstLayer) && LAYER_MAP.containsKey(secondLayer)) {
+ orderValid = false;
+ }
+ }
+ }
+ }
+
+ private static void addLayers(List providers, EnvironmentAttributeSystem.Builder systemBuilder, Level level) {
+ synchronized (LOCK) {
+ for (AttributeLayerProvider provider : providers) {
+ provider.addAttributeLayers(systemBuilder, level);
+ }
+ }
+ }
+
+ public static void addPreEverythingLayers(EnvironmentAttributeSystem.Builder systemBuilder, Level level) {
+ if (!hasOnlyVanillaLayers) {
+ sortIfNeeded();
+ addLayers(FIRST_PHASES, systemBuilder, level);
+ }
+ }
+
+ public static void addPostDimensionLayers(EnvironmentAttributeSystem.Builder systemBuilder, Level level) {
+ if (!hasOnlyVanillaLayers) {
+ sortIfNeeded();
+ addLayers(AFTER_VANILLA_PHASES.get(VanillaLayerMarker.DIMENSION), systemBuilder, level);
+ }
+ }
+
+ public static void addPostBiomesLayers(EnvironmentAttributeSystem.Builder systemBuilder, Level level) {
+ if (!hasOnlyVanillaLayers) {
+ sortIfNeeded();
+ addLayers(AFTER_VANILLA_PHASES.get(VanillaLayerMarker.BIOMES), systemBuilder, level);
+ }
+ }
+
+ public static void addPostTimelinesLayers(EnvironmentAttributeSystem.Builder systemBuilder, Level level) {
+ if (!hasOnlyVanillaLayers) {
+ sortIfNeeded();
+ addLayers(AFTER_VANILLA_PHASES.get(VanillaLayerMarker.TIMELINES), systemBuilder, level);
+ }
+ }
+
+ public static void addPostWeatherLayers(EnvironmentAttributeSystem.Builder systemBuilder, Level level) {
+ if (!hasOnlyVanillaLayers) {
+ sortIfNeeded();
+ addLayers(AFTER_VANILLA_PHASES.get(VanillaLayerMarker.WEATHER), systemBuilder, level);
+ }
+ }
+
+ private static void sortIfNeeded() {
+ Map layers;
+
+ // Collect sorting data from registry
+ synchronized (LOCK) {
+ if (orderValid) {
+ return;
+ }
+
+ layers = new HashMap<>();
+
+ for (Map.Entry entry : LAYER_MAP.entrySet()) {
+ layers.put(entry.getKey(), new Layer(entry.getKey(), entry.getValue()));
+ }
+
+ for (Dependency dependency : DEPENDENCIES) {
+ Layer firstLayer = layers.get(dependency.firstLayer());
+ Layer secondLayer = layers.get(dependency.secondLayer());
+
+ if (firstLayer != null && secondLayer != null) {
+ Layer.link(firstLayer, secondLayer);
+ }
+ }
+ }
+
+ // Sort layers
+ List sorted = new ArrayList<>(layers.values());
+ NodeSorting.sort(sorted, "environment attribute layers", AttributeLayerRegistryImpl::compareIds);
+
+ // Categorize layer providers into vanilla phases
+ synchronized (LOCK) {
+ FIRST_PHASES.clear();
+ AFTER_VANILLA_PHASES.forEach((_, list) -> list.clear());
+
+ List phase = FIRST_PHASES;
+
+ for (Layer layer : sorted) {
+ AttributeLayerProvider provider = layer.provider;
+
+ if (provider instanceof VanillaLayerMarker marker) {
+ phase = AFTER_VANILLA_PHASES.get(marker);
+ } else {
+ phase.add(provider);
+ }
+ }
+ }
+ }
+
+ // Tiebreaker: put vanilla layers before others, and otherwise sort by lexicographic ordering
+ // This also makes sure that layers that were not tied to vanilla ordering will come last in the ordering
+ private static int compareIds(Layer a, Layer b) {
+ Identifier idA = a.id;
+ Identifier idB = b.id;
+
+ VanillaLayerMarker markerA = MARKERS.get(idA);
+ VanillaLayerMarker markerB = MARKERS.get(idB);
+
+ // If both are vanilla layers, ensure they remain the same order as defined by Minecraft
+ if (markerA != null && markerB != null) {
+ return markerA.compareTo(markerB);
+ }
+
+ // If one of them is a vanilla layer and the other is not, then put the vanilla layer first
+ if (markerA != null) {
+ return -1;
+ }
+
+ if (markerB != null) {
+ return 1;
+ }
+
+ // Otherwise just mess with the mod devs that like their mod IDs to start with an A
+ return idA.compareTo(idB);
+ }
+
+ // Markers for vanilla layers. It's important that these enum constants stay in the order that vanilla layers should appear,
+ // since this order will be used to fix dependency cycles (and we don't want a dependency cycle to mess up the order).
+ private enum VanillaLayerMarker implements AttributeLayerProvider {
+ DIMENSION(AttributeLayerProvider.DIMENSION),
+ BIOMES(AttributeLayerProvider.BIOMES),
+ TIMELINES(AttributeLayerProvider.TIMELINES),
+ WEATHER(AttributeLayerProvider.WEATHER);
+
+ final Identifier id;
+
+ VanillaLayerMarker(Identifier id) {
+ this.id = id;
+ }
+
+ @Override
+ public void addAttributeLayers(EnvironmentAttributeSystem.Builder systemBuilder, Level level) {
+ // N/A, done through mixin
+ }
+ }
+
+ private static class Layer extends SortableNode {
+ private final Identifier id;
+ private final AttributeLayerProvider provider;
+
+ private Layer(Identifier id, AttributeLayerProvider provider) {
+ this.id = id;
+ this.provider = provider;
+ }
+
+ @Override
+ protected String getDescription() {
+ return id.toString();
+ }
+ }
+
+ private record Dependency(Identifier firstLayer, Identifier secondLayer) {
+ }
+}
diff --git a/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/mixin/environment/attribute/EnvironmentAttributeSystemMixin.java b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/mixin/environment/attribute/EnvironmentAttributeSystemMixin.java
new file mode 100644
index 0000000000..2afd529e37
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/main/java/net/fabricmc/fabric/mixin/environment/attribute/EnvironmentAttributeSystemMixin.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.environment.attribute;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.world.attribute.EnvironmentAttributeSystem;
+import net.minecraft.world.level.Level;
+
+import net.fabricmc.fabric.impl.environment.attribute.AttributeLayerRegistryImpl;
+
+@Mixin(EnvironmentAttributeSystem.class)
+public class EnvironmentAttributeSystemMixin {
+ @Inject(
+ method = "addDefaultLayers",
+ at = @At(value = "HEAD")
+ )
+ private static void addLayersBeforeAll(EnvironmentAttributeSystem.Builder builder, Level level, CallbackInfo ci) {
+ AttributeLayerRegistryImpl.addPreEverythingLayers(builder, level);
+ }
+
+ @Inject(
+ method = "addDefaultLayers",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/world/attribute/EnvironmentAttributeSystem;addBiomeLayer(Lnet/minecraft/world/attribute/EnvironmentAttributeSystem$Builder;Lnet/minecraft/core/HolderLookup;Lnet/minecraft/world/level/biome/BiomeManager;)V")
+ )
+ private static void addLayersAfterDimension(EnvironmentAttributeSystem.Builder builder, Level level, CallbackInfo ci) {
+ AttributeLayerRegistryImpl.addPostDimensionLayers(builder, level);
+ }
+
+ @Inject(
+ method = "addDefaultLayers",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;dimensionType()Lnet/minecraft/world/level/dimension/DimensionType;")
+ )
+ private static void addLayersAfterBiomes(EnvironmentAttributeSystem.Builder builder, Level level, CallbackInfo ci) {
+ AttributeLayerRegistryImpl.addPostBiomesLayers(builder, level);
+ }
+
+ @Inject(
+ method = "addDefaultLayers",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;canHaveWeather()Z")
+ )
+ private static void addLayersAfterTimelines(EnvironmentAttributeSystem.Builder builder, Level level, CallbackInfo ci) {
+ AttributeLayerRegistryImpl.addPostTimelinesLayers(builder, level);
+ }
+
+ @Inject(
+ method = "addDefaultLayers",
+ at = @At(value = "TAIL")
+ )
+ private static void addLayersAfterWeather(EnvironmentAttributeSystem.Builder builder, Level level, CallbackInfo ci) {
+ AttributeLayerRegistryImpl.addPostWeatherLayers(builder, level);
+ }
+}
diff --git a/fabric-environment-attributes-v1/src/main/resources/assets/fabric-environment-attributes-v1/icon.png b/fabric-environment-attributes-v1/src/main/resources/assets/fabric-environment-attributes-v1/icon.png
new file mode 100644
index 0000000000..12c4531de9
Binary files /dev/null and b/fabric-environment-attributes-v1/src/main/resources/assets/fabric-environment-attributes-v1/icon.png differ
diff --git a/fabric-environment-attributes-v1/src/main/resources/fabric-environment-attributes-v1.mixins.json b/fabric-environment-attributes-v1/src/main/resources/fabric-environment-attributes-v1.mixins.json
new file mode 100644
index 0000000000..7839519ee8
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/main/resources/fabric-environment-attributes-v1.mixins.json
@@ -0,0 +1,14 @@
+{
+ "required": true,
+ "package": "net.fabricmc.fabric.mixin.environment.attribute",
+ "compatibilityLevel": "JAVA_25",
+ "mixins": [
+ "EnvironmentAttributeSystemMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ },
+ "overwrites": {
+ "requireAnnotations": true
+ }
+}
diff --git a/fabric-environment-attributes-v1/src/main/resources/fabric.mod.json b/fabric-environment-attributes-v1/src/main/resources/fabric.mod.json
new file mode 100644
index 0000000000..23d5311560
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/main/resources/fabric.mod.json
@@ -0,0 +1,31 @@
+{
+ "schemaVersion": 1,
+ "id": "fabric-environment-attributes-v1",
+ "name": "Fabric Environment Attributes API (v1)",
+ "version": "${version}",
+ "license": "Apache-2.0",
+ "icon": "assets/fabric-environment-attributes-v1/icon.png",
+ "contact" : {
+ "homepage": "https://fabricmc.net",
+ "irc": "irc://irc.esper.net:6667/fabric",
+ "issues": "https://github.com/FabricMC/fabric/issues",
+ "sources": "https://github.com/FabricMC/fabric"
+ },
+ "authors": [
+ "FabricMC"
+ ],
+ "entrypoints": {
+ },
+ "depends": {
+ "fabricloader": ">=0.18.4",
+ "minecraft": ">=1.16-rc.3",
+ "fabric-api-base": "*"
+ },
+ "description": "Fabric Environment Attributes API.",
+ "mixins": [
+ "fabric-environment-attributes-v1.mixins.json"
+ ],
+ "custom": {
+ "fabric-api:module-lifecycle": "stable"
+ }
+}
diff --git a/fabric-environment-attributes-v1/src/testmod/java/net/fabricmc/fabric/test/environment/attribute/FabricEnvironmentAttributesTest.java b/fabric-environment-attributes-v1/src/testmod/java/net/fabricmc/fabric/test/environment/attribute/FabricEnvironmentAttributesTest.java
new file mode 100644
index 0000000000..1d7427a899
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/testmod/java/net/fabricmc/fabric/test/environment/attribute/FabricEnvironmentAttributesTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.environment.attribute;
+
+import net.minecraft.core.Registry;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.attribute.AttributeTypes;
+import net.minecraft.world.attribute.EnvironmentAttribute;
+
+import net.fabricmc.api.ModInitializer;
+
+public class FabricEnvironmentAttributesTest implements ModInitializer {
+ public static final EnvironmentAttribute TEST_COLOR = EnvironmentAttribute.builder(AttributeTypes.RGB_COLOR)
+ .defaultValue(0xFFFFFF)
+ .syncable()
+ .build();
+
+ @Override
+ public void onInitialize() {
+ Registry.register(BuiltInRegistries.ENVIRONMENT_ATTRIBUTE, Identifier.fromNamespaceAndPath("fabric_environment_attributes", "test_color"), TEST_COLOR);
+ }
+}
diff --git a/fabric-environment-attributes-v1/src/testmod/resources/fabric.mod.json b/fabric-environment-attributes-v1/src/testmod/resources/fabric.mod.json
new file mode 100644
index 0000000000..843bcd0f1d
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/testmod/resources/fabric.mod.json
@@ -0,0 +1,19 @@
+{
+ "schemaVersion": 1,
+ "id": "fabric-environment-attributes-v1-testmod",
+ "name": "Fabric Environment Attributes (v1) Test Mod",
+ "version": "1.0.0",
+ "environment": "*",
+ "license": "Apache-2.0",
+ "depends": {
+ "fabric-environment-attributes-v1": "*"
+ },
+ "entrypoints": {
+ "main": [
+ "net.fabricmc.fabric.test.environment.attribute.FabricEnvironmentAttributesTest"
+ ],
+ "fabric-client-gametest": [
+ "net.fabricmc.fabric.test.environment.attribute.client.FabricEnvironmentAttributesClientTest"
+ ]
+ }
+}
diff --git a/fabric-environment-attributes-v1/src/testmodClient/java/net/fabricmc/fabric/test/environment/attribute/client/FabricEnvironmentAttributesClientTest.java b/fabric-environment-attributes-v1/src/testmodClient/java/net/fabricmc/fabric/test/environment/attribute/client/FabricEnvironmentAttributesClientTest.java
new file mode 100644
index 0000000000..c74845bd14
--- /dev/null
+++ b/fabric-environment-attributes-v1/src/testmodClient/java/net/fabricmc/fabric/test/environment/attribute/client/FabricEnvironmentAttributesClientTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.environment.attribute.client;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.attribute.EnvironmentAttributes;
+import net.minecraft.world.level.Level;
+
+import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
+import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.context.TestSingleplayerContext;
+import net.fabricmc.fabric.api.environment.attribute.v1.AttributeLayerProvider;
+import net.fabricmc.fabric.api.environment.attribute.v1.AttributeLayerRegistry;
+import net.fabricmc.fabric.test.environment.attribute.FabricEnvironmentAttributesTest;
+
+public class FabricEnvironmentAttributesClientTest implements FabricClientGameTest {
+ public static final int TEST_COLOR = 0xFFFF00FF;
+
+ private static final Identifier BEFORE_ALL = Identifier.fromNamespaceAndPath("fabric", "before_all");
+ private static final Identifier AFTER_ALL = Identifier.fromNamespaceAndPath("fabric", "after_all");
+
+ @Override
+ public void runTest(ClientGameTestContext context) {
+ AttributeLayerRegistry.registerLayerProvider(BEFORE_ALL, (systemBuilder, level) -> {
+ // Test color is not overridden in any way, we should see the layer
+ systemBuilder.addConstantLayer(FabricEnvironmentAttributesTest.TEST_COLOR, base -> TEST_COLOR);
+
+ // Cloud color is overridden in overworld dimension, we should not see it
+ systemBuilder.addConstantLayer(EnvironmentAttributes.CLOUD_COLOR, base -> TEST_COLOR);
+ });
+
+ AttributeLayerRegistry.registerLayerProvider(AFTER_ALL, (systemBuilder, level) -> {
+ systemBuilder.addConstantLayer(EnvironmentAttributes.SKY_COLOR, base -> TEST_COLOR);
+ });
+
+ AttributeLayerRegistry.addLayerOrdering(BEFORE_ALL, AttributeLayerProvider.FIRST_VANILLA_PHASE);
+ AttributeLayerRegistry.addLayerOrdering(AttributeLayerProvider.LAST_VANILLA_PHASE, AFTER_ALL);
+
+ try (TestSingleplayerContext spContext = context.worldBuilder().create()) {
+ spContext.getServer().runOnServer(server -> {
+ ServerLevel overworld = server.getLevel(Level.OVERWORLD);
+ int testColor = overworld.environmentAttributes().getValue(FabricEnvironmentAttributesTest.TEST_COLOR, BlockPos.ZERO);
+ int cloudColor = overworld.environmentAttributes().getValue(EnvironmentAttributes.CLOUD_COLOR, BlockPos.ZERO);
+ int skyColor = overworld.environmentAttributes().getValue(EnvironmentAttributes.SKY_COLOR, BlockPos.ZERO);
+
+ if (testColor != TEST_COLOR) {
+ throw new AssertionError("Expected test color to be (%d) but was (%d)".formatted(TEST_COLOR, testColor));
+ }
+
+ if (cloudColor == TEST_COLOR) {
+ throw new AssertionError("Expected cloud color to not be (%d), but it was".formatted(TEST_COLOR));
+ }
+
+ if (skyColor != TEST_COLOR) {
+ throw new AssertionError("Expected sky color to be (%d) but was (%d)".formatted(TEST_COLOR, skyColor));
+ }
+ });
+ }
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index c37a4892b3..c68bbd1e61 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -25,6 +25,7 @@ fabric-data-generation-api-v1-version=24.0.10
fabric-debug-api-v1-version=1.0.0
fabric-dimensions-v1-version=5.1.1
fabric-entity-events-v1-version=5.0.0
+fabric-environment-attributes-v1-version=1.0.0
fabric-events-interaction-v0-version=5.1.5
fabric-game-rule-api-v1-version=4.0.3
fabric-gametest-api-v1-version=4.0.7
diff --git a/settings.gradle b/settings.gradle
index 8e249715bc..042b4bb764 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -40,6 +40,7 @@ include 'fabric-data-generation-api-v1'
include 'fabric-debug-api-v1'
include 'fabric-dimensions-v1'
include 'fabric-entity-events-v1'
+include 'fabric-environment-attributes-v1'
include 'fabric-events-interaction-v0'
include 'fabric-game-rule-api-v1'
include 'fabric-gametest-api-v1'