From 95b49bedbc4d42e3c581298281fe81ed3a236156 Mon Sep 17 00:00:00 2001 From: RealMangoRage Date: Mon, 20 May 2024 23:23:45 -0700 Subject: [PATCH] Fix crash when reloading a world that uses custom placed features. Closes #9979 --- build_forge.gradle | 2 +- .../level/chunk/ChunkGenerator.java.patch | 27 ++++++ .../common/util/ClearableLazy.java | 88 +++++++++++++++++++ .../server/ServerLifecycleHooks.java | 4 + .../add_overworld_netherite_block_ore.json | 6 ++ .../overworld_netherite_block_ore.json | 18 ++++ .../overworld_netherite_block_ore_placed.json | 15 ++++ .../debug/gameplay/biome/BiomeTestMod.java | 77 ++++++++++++++++ 8 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 patches/minecraft/net/minecraft/world/level/chunk/ChunkGenerator.java.patch create mode 100644 src/main/java/net/minecraftforge/common/util/ClearableLazy.java create mode 100644 src/test/generated/biome_test_mod/data/biome_test_mod/forge/biome_modifier/add_overworld_netherite_block_ore.json create mode 100644 src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/configured_feature/overworld_netherite_block_ore.json create mode 100644 src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/placed_feature/overworld_netherite_block_ore_placed.json create mode 100644 src/test/java/net/minecraftforge/debug/gameplay/biome/BiomeTestMod.java diff --git a/build_forge.gradle b/build_forge.gradle index c4089ff7b9..dc50beb3d7 100644 --- a/build_forge.gradle +++ b/build_forge.gradle @@ -214,6 +214,7 @@ patcher { } forge_data_test { + source sourceSets.test args '--launchTarget', 'forge_dev_data', '--mod', '.+', '--all', @@ -224,7 +225,6 @@ patcher { '--assetsDir', downloadAssets.output, '--assetIndex', "{asset_index}" } - } } diff --git a/patches/minecraft/net/minecraft/world/level/chunk/ChunkGenerator.java.patch b/patches/minecraft/net/minecraft/world/level/chunk/ChunkGenerator.java.patch new file mode 100644 index 0000000000..81c159a836 --- /dev/null +++ b/patches/minecraft/net/minecraft/world/level/chunk/ChunkGenerator.java.patch @@ -0,0 +1,27 @@ +--- a/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -81,7 +_,7 @@ + public abstract class ChunkGenerator { + public static final Codec CODEC = BuiltInRegistries.CHUNK_GENERATOR.byNameCodec().dispatchStable(ChunkGenerator::codec, Function.identity()); + protected final BiomeSource biomeSource; +- private final Supplier> featuresPerStep; ++ private final net.minecraftforge.common.util.ClearableLazy> featuresPerStep; + private final Function, BiomeGenerationSettings> generationSettingsGetter; + + public ChunkGenerator(BiomeSource p_256133_) { +@@ -91,9 +_,14 @@ + public ChunkGenerator(BiomeSource p_255838_, Function, BiomeGenerationSettings> p_256216_) { + this.biomeSource = p_255838_; + this.generationSettingsGetter = p_256216_; +- this.featuresPerStep = Suppliers.memoize( ++ this.featuresPerStep = net.minecraftforge.common.util.ClearableLazy.concurrentOf( + () -> FeatureSorter.buildFeaturesPerStep(List.copyOf(p_255838_.possibleBiomes()), p_223216_ -> p_256216_.apply(p_223216_).features(), true) + ); ++ } ++ ++ @org.jetbrains.annotations.ApiStatus.Internal ++ public void refreshFeaturesPerStep() { ++ featuresPerStep.invalidate(); + } + + public void validate() { diff --git a/src/main/java/net/minecraftforge/common/util/ClearableLazy.java b/src/main/java/net/minecraftforge/common/util/ClearableLazy.java new file mode 100644 index 0000000000..d139a7e094 --- /dev/null +++ b/src/main/java/net/minecraftforge/common/util/ClearableLazy.java @@ -0,0 +1,88 @@ +package net.minecraftforge.common.util; + + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public interface ClearableLazy extends Lazy { + void invalidate(); + + /** + * Constructs a lazy-initialized object + * @param supplier The supplier for the value, to be called the first time the value is needed. + */ + static ClearableLazy of(@NotNull Supplier supplier) { + return new ClearableLazy.Fast(supplier); + } + + /** + * Constructs a thread-safe lazy-initialized object + * @param supplier The supplier for the value, to be called the first time the value is needed. + */ + static ClearableLazy concurrentOf(@NotNull Supplier supplier) { + return new ClearableLazy.Concurrent<>(supplier); + } + + /** + * Non-thread-safe implementation. + */ + final class Fast implements ClearableLazy { + private final Supplier supplier; + private T instance; + + private Fast(Supplier supplier) { + this.supplier = supplier; + } + + @Nullable + @Override + public final T get() { + if (instance == null) { + instance = supplier.get(); + } + return instance; + } + + @Override + public void invalidate() { + this.instance = null; + } + } + + /** + * Thread-safe implementation. + */ + final class Concurrent implements ClearableLazy { + private final Object lock = new Object(); + private final Supplier supplier; + private volatile T instance; + + private Concurrent(Supplier supplier) { + this.supplier = supplier; + } + + @Nullable + @Override + public final T get() + { + var ret = instance; + if (ret == null) { + synchronized (lock) { + if (instance == null) { + return instance = supplier.get(); + } + } + } + return ret; + } + + @Override + public void invalidate() { + synchronized (lock) { + this.instance = null; + } + } + } +} diff --git a/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java b/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java index 06525a1560..448f0eda8f 100644 --- a/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java +++ b/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java @@ -132,6 +132,10 @@ public class ServerLifecycleHooks { registries.registryOrThrow(Registries.BIOME).holders().forEach(biomeHolder -> biomeHolder.value().modifiableBiomeInfo().applyBiomeModifiers(biomeHolder, biomeModifiers) ); + // Rebuild the indexed feature list + registries.registryOrThrow(Registries.LEVEL_STEM).forEach(levelStem -> { + levelStem.generator().refreshFeaturesPerStep(); + }); // Apply sorted structure modifiers to each structure. registries.registryOrThrow(Registries.STRUCTURE).holders().forEach(structureHolder -> structureHolder.value().modifiableStructureInfo().applyStructureModifiers(structureHolder, structureModifiers) diff --git a/src/test/generated/biome_test_mod/data/biome_test_mod/forge/biome_modifier/add_overworld_netherite_block_ore.json b/src/test/generated/biome_test_mod/data/biome_test_mod/forge/biome_modifier/add_overworld_netherite_block_ore.json new file mode 100644 index 0000000000..101a52c47c --- /dev/null +++ b/src/test/generated/biome_test_mod/data/biome_test_mod/forge/biome_modifier/add_overworld_netherite_block_ore.json @@ -0,0 +1,6 @@ +{ + "type": "forge:add_features", + "biomes": "#minecraft:is_overworld", + "features": "biome_test_mod:overworld_netherite_block_ore_placed", + "step": "underground_ores" +} \ No newline at end of file diff --git a/src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/configured_feature/overworld_netherite_block_ore.json b/src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/configured_feature/overworld_netherite_block_ore.json new file mode 100644 index 0000000000..4005c23ffc --- /dev/null +++ b/src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/configured_feature/overworld_netherite_block_ore.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:replace_single_block", + "config": { + "targets": [ + { + "state": { + "Name": "minecraft:netherite_block" + }, + "target": { + "block_state": { + "Name": "minecraft:air" + }, + "predicate_type": "minecraft:blockstate_match" + } + } + ] + } +} \ No newline at end of file diff --git a/src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/placed_feature/overworld_netherite_block_ore_placed.json b/src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/placed_feature/overworld_netherite_block_ore_placed.json new file mode 100644 index 0000000000..98a7c311b0 --- /dev/null +++ b/src/test/generated/biome_test_mod/data/biome_test_mod/worldgen/placed_feature/overworld_netherite_block_ore_placed.json @@ -0,0 +1,15 @@ +{ + "feature": "biome_test_mod:overworld_netherite_block_ore", + "placement": [ + { + "type": "minecraft:count", + "count": 1 + }, + { + "type": "minecraft:height_range", + "height": { + "absolute": 100 + } + } + ] +} \ No newline at end of file diff --git a/src/test/java/net/minecraftforge/debug/gameplay/biome/BiomeTestMod.java b/src/test/java/net/minecraftforge/debug/gameplay/biome/BiomeTestMod.java new file mode 100644 index 0000000000..6da5f57402 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/gameplay/biome/BiomeTestMod.java @@ -0,0 +1,77 @@ +package net.minecraftforge.debug.gameplay.biome; + +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistrySetBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.PackOutput; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.BiomeTags; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.VerticalAnchor; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.configurations.ReplaceBlockConfiguration; +import net.minecraft.world.level.levelgen.heightproviders.ConstantHeight; +import net.minecraft.world.level.levelgen.placement.CountPlacement; +import net.minecraft.world.level.levelgen.placement.HeightRangePlacement; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraftforge.common.data.DatapackBuiltinEntriesProvider; +import net.minecraftforge.common.world.BiomeModifier; +import net.minecraftforge.common.world.ForgeBiomeModifiers; +import net.minecraftforge.data.event.GatherDataEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.test.BaseTestMod; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + + +@Mod(BiomeTestMod.MOD_ID) +public class BiomeTestMod extends BaseTestMod { + public static final String MOD_ID = "biome_test_mod"; + + private static final ResourceKey OVERWORLD_NETHERITE_BLOCK_ORE_PLACED_KEY = ResourceKey.create(Registries.PLACED_FEATURE, new ResourceLocation(BiomeTestMod.MOD_ID, "overworld_netherite_block_ore_placed")); + private static final ResourceKey ADD_OVERWORLD_NETHERITE_BLOCK_ORE = ResourceKey.create(ForgeRegistries.Keys.BIOME_MODIFIERS, new ResourceLocation(BiomeTestMod.MOD_ID, "add_overworld_netherite_block_ore")); + private static final ResourceKey> OVERWORLD_NETHERITE_BLOCK_ORE = ResourceKey.create(Registries.CONFIGURED_FEATURE, new ResourceLocation(BiomeTestMod.MOD_ID, "overworld_netherite_block_ore")); + + public BiomeTestMod() { + var modBus = FMLJavaModLoadingContext.get().getModEventBus(); + modBus.addListener(BiomeTestMod::onDataGen); + } + + private static void onDataGen(GatherDataEvent event) { + event.getGenerator().addProvider(event.includeServer(), new ModWorldGenProvider(event.getGenerator().getPackOutput(), event.getLookupProvider())); + } + + private static class ModWorldGenProvider extends DatapackBuiltinEntriesProvider { + public static final RegistrySetBuilder BUILDER = new RegistrySetBuilder() + .add(Registries.CONFIGURED_FEATURE, context -> { + context.register(OVERWORLD_NETHERITE_BLOCK_ORE, new ConfiguredFeature<>( + Feature.REPLACE_SINGLE_BLOCK, + new ReplaceBlockConfiguration(Blocks.AIR.defaultBlockState(), Blocks.NETHERITE_BLOCK.defaultBlockState()) + )); + }) + .add(Registries.PLACED_FEATURE, context -> { + context.register(OVERWORLD_NETHERITE_BLOCK_ORE_PLACED_KEY, new PlacedFeature( + context.lookup(Registries.CONFIGURED_FEATURE).getOrThrow(OVERWORLD_NETHERITE_BLOCK_ORE), + List.of(CountPlacement.of(1), HeightRangePlacement.of(ConstantHeight.of(VerticalAnchor.absolute(100)))) + )); + }) + .add(ForgeRegistries.Keys.BIOME_MODIFIERS, context -> { + context.register(ADD_OVERWORLD_NETHERITE_BLOCK_ORE, new ForgeBiomeModifiers.AddFeaturesBiomeModifier( + context.lookup(Registries.BIOME).getOrThrow(BiomeTags.IS_OVERWORLD), + HolderSet.direct(context.lookup(Registries.PLACED_FEATURE).getOrThrow(OVERWORLD_NETHERITE_BLOCK_ORE_PLACED_KEY)), + GenerationStep.Decoration.UNDERGROUND_ORES + )); + }); + + public ModWorldGenProvider(PackOutput output, CompletableFuture registries) { + super(output, registries, BUILDER, Set.of(BiomeTestMod.MOD_ID)); + } + } +}