From e6162f8c971fa6cfd1fe87c51e0a289ec20c01c2 Mon Sep 17 00:00:00 2001 From: wilderion Date: Wed, 19 Aug 2026 19:04:21 +0300 Subject: [PATCH] overriding the seed for any dimension --- .../configuration/GlobalConfiguration.java | 17 +++++ .../papermc/paper/world/PaperWorldLoader.java | 8 ++- .../world/settings/PaperWorldGenSettings.java | 55 ++++++++++++++ .../settings/PaperWorldGenSettingsTest.java | 71 +++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 paper-server/src/main/java/io/papermc/paper/world/settings/PaperWorldGenSettings.java create mode 100644 paper-server/src/test/java/io/papermc/paper/world/settings/PaperWorldGenSettingsTest.java diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index 936f8388c84b..4cab98ba1e75 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -149,6 +149,23 @@ public class Console extends ConfigurationPart { public boolean hasAllPermissions = false; } + public WorldGeneration worldGeneration; + + public class WorldGeneration extends ConfigurationPart { + @Comment( + "Whether seed overrides should also be applied to dimensions that already have saved world generation settings. " + + "Existing chunks are not regenerated, so changing a dimension's seed may create abrupt chunk borders." + ) + public boolean applyToExistingDimensions = false; + + @Comment( + "Overrides the seed used to generate a dimension. " + + "Keys are dimension identifiers, for example minecraft:overworld or example:custom_dimension. " + + "By default, overrides are only applied when a dimension is created for the first time." + ) + public Map dimensionSeedOverrides = Map.of(); + } + public Watchdog watchdog; public class Watchdog extends ConfigurationPart { diff --git a/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java b/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java index 66ee5e01f189..db605badbc98 100644 --- a/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java +++ b/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java @@ -4,6 +4,7 @@ import io.papermc.paper.world.saveddata.PaperLevelOverrides; import io.papermc.paper.world.saveddata.PaperWorldMetadata; import io.papermc.paper.world.saveddata.PaperWorldPDC; +import io.papermc.paper.world.settings.PaperWorldGenSettings; import java.io.IOException; import java.nio.file.Files; import java.util.Locale; @@ -144,13 +145,18 @@ private void loadInitialWorld(final LevelStem stem, final boolean hasWorldData) return; } - final WorldGenSettings worldGenSettings = !hasWorldData + final WorldGenSettings storedWorldGenSettings = !hasWorldData ? this.server.getWorldGenSettings() : loadWorldGenSettings( this.server.storageSource, this.server.worldLoaderContext.datapackWorldgen(), loading.info().dimensionKey() ); + final WorldGenSettings worldGenSettings = PaperWorldGenSettings.applySeedOverride( + storedWorldGenSettings, + loading.info().dimensionKey(), + hasWorldData + ); final var worldDataAndGenSettings = new LevelDataAndDimensions.WorldDataAndGenSettings(this.server.getWorldData(), worldGenSettings); if (loading.info().dimensionKey() == Level.OVERWORLD) { diff --git a/paper-server/src/main/java/io/papermc/paper/world/settings/PaperWorldGenSettings.java b/paper-server/src/main/java/io/papermc/paper/world/settings/PaperWorldGenSettings.java new file mode 100644 index 000000000000..31c5efca1be4 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/settings/PaperWorldGenSettings.java @@ -0,0 +1,55 @@ +package io.papermc.paper.world.settings; + +import io.papermc.paper.configuration.GlobalConfiguration; +import java.util.Map; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.levelgen.WorldOptions; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class PaperWorldGenSettings { + + private PaperWorldGenSettings() { + } + + public static WorldGenSettings applySeedOverride( + final WorldGenSettings settings, + final ResourceKey dimensionKey, + final boolean existingDimension + ) { + final GlobalConfiguration.WorldGeneration configuration = GlobalConfiguration.get().worldGeneration; + return applySeedOverride( + settings, + dimensionKey, + existingDimension, + configuration.applyToExistingDimensions, + configuration.dimensionSeedOverrides + ); + } + + static WorldGenSettings applySeedOverride( + final WorldGenSettings settings, + final ResourceKey dimensionKey, + final boolean existingDimension, + final boolean applyToExistingDimensions, + final Map seedOverrides + ) { + if (existingDimension && !applyToExistingDimensions) { + return settings; + } + + final Long seedOverride = seedOverrides.get(dimensionKey.identifier()); + if (seedOverride == null) { + return settings; + } + + final WorldOptions options = settings.options(); + return new WorldGenSettings( + new WorldOptions(seedOverride, options.generateStructures(), options.generateBonusChest()), + settings.dimensions() + ); + } +} diff --git a/paper-server/src/test/java/io/papermc/paper/world/settings/PaperWorldGenSettingsTest.java b/paper-server/src/test/java/io/papermc/paper/world/settings/PaperWorldGenSettingsTest.java new file mode 100644 index 000000000000..0f3979cac73c --- /dev/null +++ b/paper-server/src/test/java/io/papermc/paper/world/settings/PaperWorldGenSettingsTest.java @@ -0,0 +1,71 @@ +package io.papermc.paper.world.settings; + +import java.util.Map; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.levelgen.WorldDimensions; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.levelgen.WorldOptions; +import org.bukkit.support.environment.Normal; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +@Normal +class PaperWorldGenSettingsTest { + + private static final Identifier DIMENSION_ID = Identifier.parse("test:dimension"); + private static final ResourceKey DIMENSION_KEY = ResourceKey.create(Registries.DIMENSION, DIMENSION_ID); + + @Test + void returnsOriginalSettingsWithoutOverride() { + final WorldGenSettings settings = settings(123L, true, false); + + final WorldGenSettings result = PaperWorldGenSettings.applySeedOverride(settings, DIMENSION_KEY, false, false, Map.of()); + + assertSame(settings, result); + } + + @Test + void overridesOnlySeedForNewDimension() { + final WorldGenSettings settings = settings(123L, false, true); + + final WorldGenSettings result = PaperWorldGenSettings.applySeedOverride(settings, DIMENSION_KEY, false, false, Map.of(DIMENSION_ID, 456L)); + + assertEquals(456L, result.options().seed()); + assertFalse(result.options().generateStructures()); + assertTrue(result.options().generateBonusChest()); + assertSame(settings.dimensions(), result.dimensions()); + } + + @Test + void doesNotOverrideExistingDimensionByDefault() { + final WorldGenSettings settings = settings(123L, true, false); + + final WorldGenSettings result = PaperWorldGenSettings.applySeedOverride(settings, DIMENSION_KEY, true, false, Map.of(DIMENSION_ID, 456L)); + + assertSame(settings, result); + } + + @Test + void overridesExistingDimensionWhenEnabled() { + final WorldGenSettings settings = settings(123L, true, false); + + final WorldGenSettings result = PaperWorldGenSettings.applySeedOverride(settings, DIMENSION_KEY, true, true, Map.of(DIMENSION_ID, 456L)); + + assertEquals(456L, result.options().seed()); + } + + private static WorldGenSettings settings(final long seed, final boolean generateStructures, final boolean generateBonusChest) { + return new WorldGenSettings( + new WorldOptions(seed, generateStructures, generateBonusChest), + mock(WorldDimensions.class) + ); + } +}