diff --git a/fabric-dimensions-v1/build.gradle b/fabric-dimensions-v1/build.gradle index 02b7de3dc7..8bfe314df9 100644 --- a/fabric-dimensions-v1/build.gradle +++ b/fabric-dimensions-v1/build.gradle @@ -1,5 +1,11 @@ version = getSubprojectVersion(project) +moduleDependencies(project, [ + 'fabric-api-base', + 'fabric-lifecycle-events-v1' +]) + testDependencies(project, [ - ':fabric-resource-loader-v1' + ':fabric-resource-loader-v1', + ':fabric-client-gametest-api-v1' ]) diff --git a/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/api/dimension/v1/DimensionEvents.java b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/api/dimension/v1/DimensionEvents.java new file mode 100644 index 0000000000..7a89599c52 --- /dev/null +++ b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/api/dimension/v1/DimensionEvents.java @@ -0,0 +1,45 @@ +/* + * 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.dimension.v1; + +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.world.attribute.EnvironmentAttributeMap; +import net.minecraft.world.level.dimension.DimensionType; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +/** + * Events for manipulating dimensions. + */ +public class DimensionEvents { + /** + * This event can be used to modify the environment attributes of a dimension. + * The main use case is to add modded attributes to vanilla or modded dimensions + */ + public static final Event MODIFY_ATTRIBUTES = EventFactory.createArrayBacked(ModifyAttributes.class, listeners -> (dimension, attributes, registries) -> { + for (ModifyAttributes listener : listeners) { + listener.modifyDimensionAttributes(dimension, attributes, registries); + } + }); + + @FunctionalInterface + public interface ModifyAttributes { + void modifyDimensionAttributes(Holder dimension, EnvironmentAttributeMap.Builder attributes, HolderLookup.Provider registries); + } +} diff --git a/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/impl/dimension/DimensionModificationImpl.java b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/impl/dimension/DimensionModificationImpl.java new file mode 100644 index 0000000000..ec9970a73f --- /dev/null +++ b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/impl/dimension/DimensionModificationImpl.java @@ -0,0 +1,94 @@ +/* + * 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.dimension; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import net.minecraft.core.Holder; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.RegistrationInfo; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.attribute.EnvironmentAttributeMap; +import net.minecraft.world.level.dimension.DimensionType; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.dimension.v1.DimensionEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.mixin.dimension.DimensionTypeAccessor; +import net.fabricmc.fabric.mixin.dimension.MappedRegistryAccessor; + +public class DimensionModificationImpl implements ModInitializer { + @Override + public void onInitialize() { + ServerLifecycleEvents.SERVER_STARTING.register(server -> DimensionModificationImpl.finalizeWorldGen(server.registryAccess())); + } + + public static void finalizeWorldGen(RegistryAccess registries) { + // Now that we apply dimension modifications when the server is starting, we should only ever do this once for a + // dynamic registry manager. Marking the dynamic registry manager as modified ensures a crash if the + // precondition is violated. + DimensionModificationMarker modificationTracker = (DimensionModificationMarker) registries; + modificationTracker.fabric_markDimensionsModified(); + + Registry dimensions = registries.lookupOrThrow(Registries.DIMENSION_TYPE); + + // Build a list of all dimension keys in ascending order of their raw-id to get a consistent result. + List> keys = dimensions.entrySet().stream() + .map(Map.Entry::getKey) + .sorted(Comparator.comparingInt(key -> dimensions.getId(dimensions.getValueOrThrow(key)))) + .toList(); + + for (ResourceKey key : keys) { + Holder.Reference reference = dimensions.getOrThrow(key); + + if (applyChanges(reference, registries)) { + // Re-freeze and apply certain cleanup actions + if (dimensions instanceof MappedRegistry registry) { + Map, RegistrationInfo> registrationInfos = ((MappedRegistryAccessor) registry).fabric_getRegistrationInfos(); + RegistrationInfo info = registrationInfos.get(key); + RegistrationInfo newInfo = new RegistrationInfo(Optional.empty(), info.lifecycle()); + registrationInfos.put(key, newInfo); + } + } + } + } + + /** + * Applies the changes from the events of {@link DimensionEvents} to a dimension. + * + * @return true if the dimension was changed + */ + private static boolean applyChanges(Holder dimension, RegistryAccess registries) { + EnvironmentAttributeMap oldAttributes = dimension.value().attributes(); + EnvironmentAttributeMap.Builder attributeBuilder = EnvironmentAttributeMap.builder().putAll(oldAttributes); + DimensionEvents.MODIFY_ATTRIBUTES.invoker().modifyDimensionAttributes(dimension, attributeBuilder, registries); + EnvironmentAttributeMap newAttributes = attributeBuilder.build(); + boolean changed = !oldAttributes.equals(newAttributes); + + if (changed) { + ((DimensionTypeAccessor) (Object) dimension.value()).fabric_setAttributes(newAttributes); + } + + return changed; + } +} diff --git a/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/impl/dimension/DimensionModificationMarker.java b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/impl/dimension/DimensionModificationMarker.java new file mode 100644 index 0000000000..0af16d13ea --- /dev/null +++ b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/impl/dimension/DimensionModificationMarker.java @@ -0,0 +1,25 @@ +/* + * 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.dimension; + +/** + * Prevents double-modification of dimensions in the same dynamic registry manager from occurring and fails-fast + * if it does occur. + */ +public interface DimensionModificationMarker { + void fabric_markDimensionsModified(); +} diff --git a/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/DimensionTypeAccessor.java b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/DimensionTypeAccessor.java new file mode 100644 index 0000000000..3c324fa274 --- /dev/null +++ b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/DimensionTypeAccessor.java @@ -0,0 +1,31 @@ +/* + * 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.dimension; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.world.attribute.EnvironmentAttributeMap; +import net.minecraft.world.level.dimension.DimensionType; + +@Mixin(DimensionType.class) +public interface DimensionTypeAccessor { + @Accessor("attributes") + @Mutable + void fabric_setAttributes(EnvironmentAttributeMap attributes); +} diff --git a/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/MappedRegistryAccessor.java b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/MappedRegistryAccessor.java new file mode 100644 index 0000000000..3d51c05eff --- /dev/null +++ b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/MappedRegistryAccessor.java @@ -0,0 +1,32 @@ +/* + * 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.dimension; + +import java.util.Map; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.RegistrationInfo; +import net.minecraft.resources.ResourceKey; + +@Mixin(MappedRegistry.class) +public interface MappedRegistryAccessor { + @Accessor("registrationInfos") + Map, RegistrationInfo> fabric_getRegistrationInfos(); +} diff --git a/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/RegistryAccessImmutableRegistryAccessMixin.java b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/RegistryAccessImmutableRegistryAccessMixin.java new file mode 100644 index 0000000000..36742248b7 --- /dev/null +++ b/fabric-dimensions-v1/src/main/java/net/fabricmc/fabric/mixin/dimension/RegistryAccessImmutableRegistryAccessMixin.java @@ -0,0 +1,44 @@ +/* + * 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.dimension; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import net.minecraft.core.RegistryAccess; + +import net.fabricmc.fabric.impl.dimension.DimensionModificationImpl; +import net.fabricmc.fabric.impl.dimension.DimensionModificationMarker; + +/** + * This Mixin allows us to prevent double-modifications of dimensions via + * {@link DimensionModificationImpl} on a per-DynamicRegistryManager basis. + */ +@Mixin(RegistryAccess.ImmutableRegistryAccess.class) +public class RegistryAccessImmutableRegistryAccessMixin implements DimensionModificationMarker { + @Unique + private boolean dimensionsModified; + + @Override + public void fabric_markDimensionsModified() { + if (dimensionsModified) { + throw new IllegalStateException("Dimensions in this dynamic registries instance have already been modified"); + } + + dimensionsModified = true; + } +} diff --git a/fabric-dimensions-v1/src/main/resources/fabric-dimensions-v1.mixins.json b/fabric-dimensions-v1/src/main/resources/fabric-dimensions-v1.mixins.json index 093a12b6ad..61199ecd49 100644 --- a/fabric-dimensions-v1/src/main/resources/fabric-dimensions-v1.mixins.json +++ b/fabric-dimensions-v1/src/main/resources/fabric-dimensions-v1.mixins.json @@ -6,7 +6,10 @@ "WorldDimensionsMixin", "V2832Mixin", "TaggedChoiceMixin", - "TaggedChoiceTaggedChoiceTypeMixin" + "TaggedChoiceTaggedChoiceTypeMixin", + "DimensionTypeAccessor", + "MappedRegistryAccessor", + "RegistryAccessImmutableRegistryAccessMixin" ], "injectors": { "defaultRequire": 1 diff --git a/fabric-dimensions-v1/src/main/resources/fabric.mod.json b/fabric-dimensions-v1/src/main/resources/fabric.mod.json index b11ab2021a..0a4eb0fdc7 100644 --- a/fabric-dimensions-v1/src/main/resources/fabric.mod.json +++ b/fabric-dimensions-v1/src/main/resources/fabric.mod.json @@ -14,6 +14,11 @@ "authors": [ "FabricMC" ], + "entrypoints": { + "main": [ + "net.fabricmc.fabric.impl.dimension.DimensionModificationImpl" + ] + }, "depends": { "fabricloader": ">=0.18.3", "minecraft": ">=1.16-rc.3", diff --git a/fabric-dimensions-v1/src/testmod/resources/fabric.mod.json b/fabric-dimensions-v1/src/testmod/resources/fabric.mod.json index c2b7587add..26012c3812 100644 --- a/fabric-dimensions-v1/src/testmod/resources/fabric.mod.json +++ b/fabric-dimensions-v1/src/testmod/resources/fabric.mod.json @@ -11,6 +11,9 @@ "entrypoints": { "main": [ "net.fabricmc.fabric.test.dimension.FabricDimensionTest" + ], + "fabric-client-gametest": [ + "net.fabricmc.fabric.test.dimension.client.FabricDimensionClientTest" ] } } diff --git a/fabric-dimensions-v1/src/testmodClient/java/net/fabricmc/fabric/test/dimension/client/FabricDimensionClientTest.java b/fabric-dimensions-v1/src/testmodClient/java/net/fabricmc/fabric/test/dimension/client/FabricDimensionClientTest.java new file mode 100644 index 0000000000..83880cfa57 --- /dev/null +++ b/fabric-dimensions-v1/src/testmodClient/java/net/fabricmc/fabric/test/dimension/client/FabricDimensionClientTest.java @@ -0,0 +1,53 @@ +/* + * 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.dimension.client; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.attribute.EnvironmentAttributes; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; + +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.dimension.v1.DimensionEvents; + +public class FabricDimensionClientTest implements FabricClientGameTest { + public static final int PURPLE = 0xFFE580FF; + + @Override + public void runTest(ClientGameTestContext context) { + DimensionEvents.MODIFY_ATTRIBUTES.register((dimension, attributes, _) -> { + if (dimension.is(BuiltinDimensionTypes.OVERWORLD)) { + attributes.set(EnvironmentAttributes.CLOUD_COLOR, PURPLE); + } + }); + + try (TestSingleplayerContext spContext = context.worldBuilder().create()) { + spContext.getServer().runOnServer(server -> { + ServerLevel overworld = server.getLevel(Level.OVERWORLD); + overworld.setDayTime(6000); + int overworldCloudColor = overworld.environmentAttributes().getValue(EnvironmentAttributes.CLOUD_COLOR, BlockPos.ZERO); + + if (overworldCloudColor != PURPLE) { + throw new AssertionError("Expected overworld cloud color to be (%d) but was (%d)".formatted(PURPLE, overworldCloudColor)); + } + }); + } + } +}