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'