From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Wed, 8 Jun 2022 22:20:16 -0700
Subject: [PATCH] Paper config files


diff --git a/build.gradle.kts b/build.gradle.kts
index 5d8a84341ab5be52b5c37737e3f82590f06f6073..cfdb20447e6ad3efcdee8889712f77931773beaf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,6 +12,7 @@ dependencies {
     implementation("org.apache.logging.log4j:log4j-iostreams:2.17.1") // Paper
     implementation("org.ow2.asm:asm:9.3")
     implementation("org.ow2.asm:asm-commons:9.3") // Paper - ASM event executor generation
+    implementation("org.spongepowered:configurate-yaml:4.1.2") // Paper - config files
     implementation("commons-lang:commons-lang:2.6")
     runtimeOnly("org.xerial:sqlite-jdbc:3.36.0.3")
     runtimeOnly("mysql:mysql-connector-java:8.0.29")
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef41cf3a7d1e6f2bfe81e0fb865d2f969bbc77c1
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -0,0 +1,8 @@
+package com.destroystokyo.paper;
+
+/**
+ * @deprecated kept as a means to identify Paper in older plugins/PaperLib
+ */
+@Deprecated(forRemoval = true)
+public class PaperConfig {
+}
diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..c91f109b4cf64dc1b4ef09f38e1cb8bf5cb2be13
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -0,0 +1,8 @@
+package com.destroystokyo.paper;
+
+/**
+ * @deprecated kept as a means to identify Paper in older plugins/PaperLib
+ */
+@Deprecated(forRemoval = true)
+public class PaperWorldConfig {
+}
diff --git a/src/main/java/io/papermc/paper/configuration/Configuration.java b/src/main/java/io/papermc/paper/configuration/Configuration.java
new file mode 100644
index 0000000000000000000000000000000000000000..817fd26cc3591f9cae0f61f4036dde43c4ed60e8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/Configuration.java
@@ -0,0 +1,13 @@
+package io.papermc.paper.configuration;
+
+public final class Configuration {
+    public static final String VERSION_FIELD = "_version";
+    @Deprecated
+    public static final String LEGACY_CONFIG_VERSION_FIELD = "config-version";
+
+    @Deprecated
+    public static final int FINAL_LEGACY_VERSION = 27;
+
+    private Configuration() {
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java
new file mode 100644
index 0000000000000000000000000000000000000000..227039a6c69c4c99bbd9c674b3aab0ef5e2c1374
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java
@@ -0,0 +1,27 @@
+package io.papermc.paper.configuration;
+
+import java.nio.file.Path;
+import org.spongepowered.configurate.loader.HeaderMode;
+import org.spongepowered.configurate.util.MapFactories;
+import org.spongepowered.configurate.yaml.NodeStyle;
+import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
+
+public final class ConfigurationLoaders {
+    private ConfigurationLoaders() {
+    }
+
+    public static YamlConfigurationLoader.Builder naturallySorted() {
+        return YamlConfigurationLoader.builder()
+            .indent(2)
+            .nodeStyle(NodeStyle.BLOCK)
+            .headerMode(HeaderMode.PRESET)
+            .defaultOptions(options -> options.mapFactory(MapFactories.sortedNatural()));
+    }
+
+    public static YamlConfigurationLoader naturallySortedWithoutHeader(final Path path) {
+        return naturallySorted()
+            .headerMode(HeaderMode.NONE)
+            .path(path)
+            .build();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a4a7a654fe2516ed894a68f2657344df9d70f4c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java
@@ -0,0 +1,10 @@
+package io.papermc.paper.configuration;
+
+abstract class ConfigurationPart {
+
+    public static abstract class Post extends ConfigurationPart {
+
+        public abstract void postProcess();
+    }
+
+}
diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java
new file mode 100644
index 0000000000000000000000000000000000000000..31325994ab441c59a4c0bd9f3f9db3d9440375d0
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/Configurations.java
@@ -0,0 +1,296 @@
+package io.papermc.paper.configuration;
+
+import io.leangen.geantyref.TypeToken;
+import io.papermc.paper.configuration.constraint.Constraint;
+import io.papermc.paper.configuration.constraint.Constraints;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.level.ServerLevel;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.MustBeInvokedByOverriders;
+import org.spongepowered.configurate.CommentedConfigurationNode;
+import org.spongepowered.configurate.ConfigurateException;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.ConfigurationOptions;
+import org.spongepowered.configurate.objectmapping.ObjectMapper;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.util.CheckedFunction;
+import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.UnaryOperator;
+
+public abstract class Configurations<G, W> {
+
+    public static final String WORLD_DEFAULTS = "__world_defaults__";
+    public static final ResourceLocation WORLD_DEFAULTS_KEY = new ResourceLocation("configurations", WORLD_DEFAULTS);
+    protected final Path globalFolder;
+    protected final Class<G> globalConfigClass;
+    protected final Class<W> worldConfigClass;
+    protected final String globalConfigFileName;
+    protected final String defaultWorldConfigFileName;
+    protected final String worldConfigFileName;
+
+    public Configurations(
+        final Path globalFolder,
+        final Class<G> globalConfigType,
+        final Class<W> worldConfigClass,
+        final String globalConfigFileName,
+        final String defaultWorldConfigFileName,
+        final String worldConfigFileName
+    ) {
+        this.globalFolder = globalFolder;
+        this.globalConfigClass = globalConfigType;
+        this.worldConfigClass = worldConfigClass;
+        this.globalConfigFileName = globalConfigFileName;
+        this.defaultWorldConfigFileName = defaultWorldConfigFileName;
+        this.worldConfigFileName = worldConfigFileName;
+    }
+
+    protected ObjectMapper.Factory.Builder createObjectMapper() {
+        return ObjectMapper.factoryBuilder()
+            .addConstraint(Constraint.class, new Constraint.Factory())
+            .addConstraint(Constraints.Min.class, Number.class, new Constraints.Min.Factory());
+    }
+
+    protected YamlConfigurationLoader.Builder createLoaderBuilder() {
+        return ConfigurationLoaders.naturallySorted();
+    }
+
+    protected abstract boolean isConfigType(final Type type);
+
+    protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() {
+        return this.createObjectMapper();
+    }
+
+    @MustBeInvokedByOverriders
+    protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() {
+        return this.createLoaderBuilder();
+    }
+
+    static <T> CheckedFunction<ConfigurationNode, T, SerializationException> creator(Class<T> type, boolean refreshNode) {
+        return node -> {
+            T instance = node.require(type);
+            if (refreshNode) {
+                node.set(type, instance);
+            }
+            return instance;
+        };
+    }
+
+    static <T> CheckedFunction<ConfigurationNode, T, SerializationException> reloader(Class<T> type, T instance) {
+        return node -> {
+            ObjectMapper.Factory factory = (ObjectMapper.Factory) Objects.requireNonNull(node.options().serializers().get(type));
+            ObjectMapper.Mutable<T> mutable = (ObjectMapper.Mutable<T>) factory.get(type);
+            mutable.load(instance, node);
+            return instance;
+        };
+    }
+
+    public G initializeGlobalConfiguration() throws ConfigurateException {
+        return this.initializeGlobalConfiguration(creator(this.globalConfigClass, true));
+    }
+
+    protected G initializeGlobalConfiguration(final CheckedFunction<ConfigurationNode, G, SerializationException> creator) throws ConfigurateException {
+        final Path configFile = this.globalFolder.resolve(this.globalConfigFileName);
+        final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder()
+            .defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build()))
+            .path(configFile)
+            .build();
+        final ConfigurationNode node;
+        if (Files.exists(configFile)) {
+            node = loader.load();
+        } else {
+            node = CommentedConfigurationNode.root(loader.defaultOptions());
+        }
+        this.applyGlobalConfigTransformations(node);
+        final G instance = creator.apply(node);
+        loader.save(node);
+        return instance;
+    }
+
+    protected void applyGlobalConfigTransformations(final ConfigurationNode node) throws ConfigurateException {
+    }
+
+    @MustBeInvokedByOverriders
+    protected ContextMap.Builder createDefaultContextMap() {
+        return ContextMap.builder()
+            .put(WORLD_NAME, WORLD_DEFAULTS)
+            .put(WORLD_KEY, WORLD_DEFAULTS_KEY);
+    }
+
+    public void initializeWorldDefaultsConfiguration() throws ConfigurateException {
+        final ContextMap contextMap = this.createDefaultContextMap()
+            .put(FIRST_DEFAULT)
+            .build();
+        final DefaultWorldLoader result = this.createDefaultWorldLoader(false, contextMap);
+        final YamlConfigurationLoader loader = result.loader();
+        final ConfigurationNode node = loader.load();
+        if (result.isNewFile()) { // add version to new files
+            node.node(Configuration.VERSION_FIELD).raw(WorldConfiguration.CURRENT_VERSION);
+        }
+        this.applyWorldConfigTransformations(contextMap, node);
+        final W instance = node.require(this.worldConfigClass);
+        node.set(this.worldConfigClass, instance);
+        loader.save(node);
+    }
+
+    private DefaultWorldLoader createDefaultWorldLoader(final boolean requireFile, final ContextMap contextMap) {
+        final Path configFile = this.globalFolder.resolve(this.defaultWorldConfigFileName);
+        boolean willCreate = Files.notExists(configFile);
+        if (requireFile && willCreate) {
+            throw new IllegalStateException("World defaults configuration file '" + configFile + "' doesn't exist");
+        }
+        return new DefaultWorldLoader(
+            this.createWorldConfigLoaderBuilder(contextMap)
+                .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build()))
+                .path(configFile)
+                .build(),
+            willCreate
+        );
+    }
+
+    private record DefaultWorldLoader(YamlConfigurationLoader loader, boolean isNewFile) {
+    }
+
+    protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) {
+        return this.createObjectMapper();
+    }
+
+    @MustBeInvokedByOverriders
+    protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) {
+        return this.createLoaderBuilder();
+    }
+
+    // Make sure to run version transforms on the default world config first via #setupWorldDefaultsConfig
+    public W createWorldConfig(final ContextMap contextMap) throws IOException {
+        return this.createWorldConfig(contextMap, creator(this.worldConfigClass, false));
+    }
+
+    protected W createWorldConfig(final ContextMap contextMap, final CheckedFunction<ConfigurationNode, W, SerializationException> creator) throws IOException {
+        final YamlConfigurationLoader defaultsLoader = this.createDefaultWorldLoader(true, this.createDefaultContextMap().build()).loader();
+        final ConfigurationNode defaultsNode = defaultsLoader.load();
+
+        boolean newFile = false;
+        final Path dir = contextMap.require(WORLD_DIRECTORY);
+        final Path worldConfigFile = dir.resolve(this.worldConfigFileName);
+        if (Files.notExists(worldConfigFile)) {
+            PaperConfigurations.createDirectoriesSymlinkAware(dir);
+            Files.createFile(worldConfigFile); // create empty file as template
+            newFile = true;
+        }
+
+        final YamlConfigurationLoader worldLoader = this.createWorldConfigLoaderBuilder(contextMap)
+            .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build()))
+            .path(worldConfigFile)
+            .build();
+        final ConfigurationNode worldNode = worldLoader.load();
+        if (newFile) { // set the version field if new file
+            worldNode.node(Configuration.VERSION_FIELD).set(WorldConfiguration.CURRENT_VERSION);
+        }
+        this.applyWorldConfigTransformations(contextMap, worldNode);
+        this.applyDefaultsAwareWorldConfigTransformations(contextMap, worldNode, defaultsNode);
+        worldLoader.save(worldNode); // save before loading node NOTE: don't save the backing node after loading it, or you'll fill up the world-specific config
+        worldNode.mergeFrom(defaultsNode);
+        return creator.apply(worldNode);
+    }
+
+    protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException {
+    }
+
+    protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException {
+    }
+
+    private UnaryOperator<ConfigurationOptions> applyObjectMapperFactory(final ObjectMapper.Factory factory) {
+        return options -> options.serializers(builder -> builder
+            .register(this::isConfigType, factory.asTypeSerializer())
+            .registerAnnotatedObjects(factory));
+    }
+
+    public Path getWorldConfigFile(ServerLevel level) {
+        return level.convertable.levelDirectory.path().resolve(this.worldConfigFileName);
+    }
+
+    public static class ContextMap {
+        private static final Object VOID = new Object();
+
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        private final Map<ContextKey<?>, Object> backingMap;
+
+        private ContextMap(Map<ContextKey<?>, Object> map) {
+            this.backingMap = Map.copyOf(map);
+        }
+
+        @SuppressWarnings("unchecked")
+        public <T> T require(ContextKey<T> key) {
+            final @Nullable Object value = this.backingMap.get(key);
+            if (value == null) {
+                throw new NoSuchElementException("No element found for " + key + " with type " + key.type());
+            } else if (value == VOID) {
+                throw new IllegalArgumentException("Cannot get the value of a Void key");
+            }
+            return (T) value;
+        }
+
+        @SuppressWarnings("unchecked")
+        public <T> @Nullable T get(ContextKey<T> key) {
+            return (T) this.backingMap.get(key);
+        }
+
+        public boolean has(ContextKey<?> key) {
+            return this.backingMap.containsKey(key);
+        }
+
+        public boolean isDefaultWorldContext() {
+            return this.require(WORLD_KEY).equals(WORLD_DEFAULTS_KEY);
+        }
+
+        public static class Builder {
+
+            private Builder() {
+            }
+
+            private final Map<ContextKey<?>, Object> buildingMap = new HashMap<>();
+
+            public <T> Builder put(ContextKey<T> key, T value) {
+                this.buildingMap.put(key, value);
+                return this;
+            }
+
+            public Builder put(ContextKey<Void> key) {
+                this.buildingMap.put(key, VOID);
+                return this;
+            }
+
+            public ContextMap build() {
+                return new ContextMap(this.buildingMap);
+            }
+        }
+    }
+
+    public static final ContextKey<Path> WORLD_DIRECTORY = new ContextKey<>(Path.class, "world directory");
+    public static final ContextKey<String> WORLD_NAME = new ContextKey<>(String.class, "world name"); // TODO remove when we deprecate level names
+    public static final ContextKey<ResourceLocation> WORLD_KEY = new ContextKey<>(ResourceLocation.class, "world key");
+    public static final ContextKey<Void> FIRST_DEFAULT = new ContextKey<>(Void.class, "first default");
+
+    public record ContextKey<T>(TypeToken<T> type, String name) {
+
+        public ContextKey(Class<T> type, String name) {
+            this(TypeToken.get(type), name);
+        }
+
+        @Override
+        public String toString() {
+            return "ContextKey{" + this.name + "}";
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..456595e4b7e0c7f50617aa2694b0d2dfc368ab81
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -0,0 +1,265 @@
+package io.papermc.paper.configuration;
+
+import co.aikar.timings.MinecraftTimings;
+import com.destroystokyo.paper.io.chunk.ChunkTaskManager;
+import io.papermc.paper.configuration.constraint.Constraint;
+import io.papermc.paper.configuration.constraint.Constraints;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Comment;
+import org.spongepowered.configurate.objectmapping.meta.Required;
+import org.spongepowered.configurate.objectmapping.meta.Setting;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
+public class GlobalConfiguration extends ConfigurationPart {
+    static final int CURRENT_VERSION = 28;
+    private static GlobalConfiguration instance;
+    public static GlobalConfiguration get() {
+        return instance;
+    }
+    static void set(GlobalConfiguration instance) {
+        GlobalConfiguration.instance = instance;
+    }
+
+    @Setting(Configuration.VERSION_FIELD)
+    public int version = CURRENT_VERSION;
+
+    public Messages messages;
+
+    public class Messages extends ConfigurationPart {
+        public Kick kick;
+
+        public class Kick extends ConfigurationPart {
+            public Component authenticationServersDown = Component.translatable("multiplayer.disconnect.authservers_down");
+            public Component connectionThrottle = Component.text("Connection throttled! Please wait before reconnecting.");
+            public Component flyingPlayer = Component.translatable("multiplayer.disconnect.flying");
+            public Component flyingVehicle = Component.translatable("multiplayer.disconnect.flying");
+        }
+
+        public Component noPermission = Component.text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", NamedTextColor.RED);
+        public boolean useDisplayNameInQuitMessage = false;
+    }
+
+    public Timings timings;
+
+    public class Timings extends ConfigurationPart.Post {
+        public boolean enabled = true;
+        public boolean verbose = true;
+        public String url = "https://timings.aikar.co/";
+        public boolean serverNamePrivacy = false;
+        public List<String> hiddenConfigEntries = List.of(
+            "database",
+            "proxies.velocity.secret"
+        );
+        public int historyInterval = 300;
+        public int historyLength = 3600;
+        public String serverName = "Unknown Server";
+
+        @Override
+        public void postProcess() {
+            MinecraftTimings.processConfig(this);
+        }
+    }
+
+    public Proxies proxies;
+
+    public class Proxies extends ConfigurationPart {
+        public BungeeCord bungeeCord;
+
+        public class BungeeCord extends ConfigurationPart {
+            public boolean onlineMode = true;
+        }
+
+        @Constraint(Constraints.Velocity.class)
+        public Velocity velocity;
+
+        public class Velocity extends ConfigurationPart {
+            public boolean enabled = false;
+            public boolean onlineMode = false;
+            public String secret = "";
+        }
+        public boolean proxyProtocol = false;
+        public boolean isProxyOnlineMode() {
+            return org.bukkit.Bukkit.getOnlineMode() || (org.spigotmc.SpigotConfig.bungee && this.bungeeCord.onlineMode) || (this.velocity.enabled && this.velocity.onlineMode);
+        }
+    }
+
+    public Console console;
+
+    public class Console extends ConfigurationPart {
+        public boolean enableBrigadierHighlighting = true;
+        public boolean enableBrigadierCompletions = true;
+        public boolean hasAllPermissions = false;
+    }
+
+    public Watchdog watchdog;
+
+    public class Watchdog extends ConfigurationPart {
+        public int earlyWarningEvery = 5000;
+        public int earlyWarningDelay = 10000;
+    }
+
+    public SpamLimiter spamLimiter;
+
+    public class SpamLimiter extends ConfigurationPart {
+        public int tabSpamIncrement = 1;
+        public int tabSpamLimit = 500;
+        public int recipeSpamIncrement = 1;
+        public int recipeSpamLimit = 20;
+        public int incomingPacketThreshold = 300;
+    }
+
+    public ChunkLoading chunkLoading;
+
+    public class ChunkLoading extends ConfigurationPart {
+        public int minLoadRadius = 2;
+        public int maxConcurrentSends = 2;
+        public boolean autoconfigSendDistance = true;
+        public double targetPlayerChunkSendRate = 100.0;
+        public double globalMaxChunkSendRate = -1.0;
+        public boolean enableFrustumPriority = false;
+        public double globalMaxChunkLoadRate = -1.0;
+        public double playerMaxConcurrentLoads = 20.0;
+        public double globalMaxConcurrentLoads = 500.0;
+        public double playerMaxChunkLoadRate = -1.0;
+    }
+
+    public UnsupportedSettings unsupportedSettings;
+
+    public class UnsupportedSettings extends ConfigurationPart {
+        @Comment("This setting controls if players should be able to break bedrock, end portals and other intended to be permanent blocks.")
+        public boolean allowPermanentBlockBreakExploits = false;
+        @Comment("This setting controls if player should be able to use TNT duplication, but this also allows duplicating carpet, rails and potentially other items")
+        public boolean allowPistonDuplication = false;
+        public boolean performUsernameValidation = true;
+        @Comment("This setting controls if players should be able to create headless pistons.")
+        public boolean allowHeadlessPistons = false;
+    }
+
+    public Commands commands;
+
+    public class Commands extends ConfigurationPart {
+        public boolean suggestPlayerNamesWhenNullTabCompletions = true;
+        public boolean fixTargetSelectorTagCompletion = true;
+        public boolean timeCommandAffectsAllWorlds = false;
+    }
+
+    public Logging logging;
+
+    public class Logging extends ConfigurationPart {
+        public boolean logPlayerIpAddresses = true;
+        public boolean deobfuscateStacktraces = true;
+        public boolean useRgbForNamedTextColors = true;
+    }
+
+    public Scoreboards scoreboards;
+
+    public class Scoreboards extends ConfigurationPart {
+        public boolean trackPluginScoreboards = false;
+        public boolean saveEmptyScoreboardTeams = false;
+    }
+
+    public AsyncChunks asyncChunks;
+
+    public class AsyncChunks extends ConfigurationPart.Post {
+        public int threads = -1;
+        public transient boolean asyncChunks = false;
+
+        @Override
+        public void postProcess() {
+            ChunkTaskManager.processConfiguration(this);
+        }
+    }
+
+    public ItemValidation itemValidation;
+
+    public class ItemValidation extends ConfigurationPart {
+        public int displayName = 8192;
+        public int loreLine = 8192;
+        public Book book;
+
+        public class Book extends ConfigurationPart {
+            public int title = 8192;
+            public int author = 8192;
+            public int page = 16384;
+        }
+
+        public BookSize bookSize;
+
+        public class BookSize extends ConfigurationPart {
+            public int pageMax = 2560; // TODO this appears to be a duplicate setting with one above
+            public double totalMultiplier = 0.98D; // TODO this should probably be merged into the above inner class
+        }
+        public boolean resolveSelectorsInBooks = false;
+    }
+
+    public PacketLimiter packetLimiter;
+
+    public class PacketLimiter extends ConfigurationPart {
+        public Component kickMessage = Component.translatable("disconnect.exceeded_packet_rate", NamedTextColor.RED);
+        public PacketLimit allPackets = new PacketLimit(7.0, 500.0, PacketLimit.ViolateAction.KICK);
+        public Map<Class<? extends Packet<?>>, PacketLimit> overrides = Map.of(ServerboundPlaceRecipePacket.class, new PacketLimit(4.0, 5.0, PacketLimit.ViolateAction.DROP));
+
+        @ConfigSerializable
+        public record PacketLimit(@Required double interval, @Required double maxPacketRate, ViolateAction action) {
+            public PacketLimit(final double interval, final double maxPacketRate, final @Nullable ViolateAction action) {
+                this.interval = interval;
+                this.maxPacketRate = maxPacketRate;
+                this.action = Objects.requireNonNullElse(action, ViolateAction.KICK);
+            }
+
+            public boolean isEnabled() {
+                return this.interval > 0.0 && this.maxPacketRate > 0.0;
+            }
+
+            public enum ViolateAction {
+                KICK,
+                DROP;
+            }
+        }
+    }
+
+    public Collisions collisions;
+
+    public class Collisions extends ConfigurationPart {
+        public boolean enablePlayerCollisions = true;
+        public boolean sendFullPosForHardCollidingEntities = true;
+    }
+
+    public PlayerAutoSave playerAutoSave;
+
+
+    public class PlayerAutoSave extends ConfigurationPart {
+        public int rate = -1;
+        private int maxPerTick = -1;
+        public int maxPerTick() {
+            if (this.maxPerTick < 0) {
+                return (this.rate == 1 || this.rate > 100) ? 10 : 20;
+            }
+            return this.maxPerTick;
+        }
+    }
+
+    public Misc misc;
+
+    public class Misc extends ConfigurationPart {
+        public int maxJoinsPerTick = 3;
+        public boolean fixEntityPositionDesync = true;
+        public boolean loadPermissionsYmlBeforePlugins = true;
+        @Constraints.Min(4)
+        public int regionFileCacheSize = 256;
+        @Comment("See https://luckformula.emc.gs")
+        public boolean useAlternativeLuckFormula = false;
+        public boolean lagCompensateBlockBreaking = true;
+        public boolean useDimensionTypeForCustomSpawners = false;
+        public boolean strictAdvancementDimensionCheck = false;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0aa1f1a7adf986d500a2135aa42e138aa3c4f08
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java
@@ -0,0 +1,142 @@
+package io.papermc.paper.configuration;
+
+import io.leangen.geantyref.GenericTypeReflector;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.objectmapping.FieldDiscoverer;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.util.CheckedSupplier;
+
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import static io.leangen.geantyref.GenericTypeReflector.erase;
+
+final class InnerClassFieldDiscoverer implements FieldDiscoverer<Map<Field, Object>> {
+
+    private final Map<Class<?>, Object> instanceMap = new HashMap<>();
+    private final Map<Class<?>, Object> overrides;
+    @SuppressWarnings("unchecked")
+    private final FieldDiscoverer<Map<Field, Object>> delegate = (FieldDiscoverer<Map<Field, Object>>) FieldDiscoverer.object(target -> {
+        final Class<?> type = erase(target.getType());
+        if (this.overrides().containsKey(type)) {
+            this.instanceMap.put(type, this.overrides().get(type));
+            return () -> this.overrides().get(type);
+        }
+        if (ConfigurationPart.class.isAssignableFrom(type) && !this.instanceMap.containsKey(type)) {
+            try {
+                final Constructor<?> constructor;
+                final CheckedSupplier<Object, ReflectiveOperationException> instanceSupplier;
+                if (type.getEnclosingClass() != null && !Modifier.isStatic(type.getModifiers())) {
+                    final @Nullable Object instance = this.instanceMap.get(type.getEnclosingClass());
+                    if (instance == null) {
+                        throw new SerializationException("Cannot create a new instance of an inner class " + type.getName() + " without an instance of its enclosing class " + type.getEnclosingClass().getName());
+                    }
+                    constructor = type.getDeclaredConstructor(type.getEnclosingClass());
+                    instanceSupplier = () -> constructor.newInstance(instance);
+                } else {
+                    constructor = type.getDeclaredConstructor();
+                    instanceSupplier = constructor::newInstance;
+                }
+                constructor.setAccessible(true);
+                final Object instance = instanceSupplier.get();
+                this.instanceMap.put(type, instance);
+                return () -> instance;
+            } catch (ReflectiveOperationException e) {
+                throw new SerializationException(ConfigurationPart.class, target + " must be a valid ConfigurationPart", e);
+            }
+        } else {
+            throw new SerializationException(target + " must be a valid ConfigurationPart");
+        }
+    }, "Object must be a unique ConfigurationPart");
+
+    InnerClassFieldDiscoverer(Map<Class<?>, Object> overrides) {
+        this.overrides = overrides;
+    }
+
+    @Override
+    public @Nullable <V> InstanceFactory<Map<Field, Object>> discover(AnnotatedType target, FieldCollector<Map<Field, Object>, V> collector) throws SerializationException {
+        final Class<?> clazz = erase(target.getType());
+        if (ConfigurationPart.class.isAssignableFrom(clazz)) {
+            final FieldDiscoverer.@Nullable InstanceFactory<Map<Field, Object>> instanceFactoryDelegate = this.delegate.<V>discover(target, (name, type, annotations, deserializer, serializer) -> {
+                if (!erase(type.getType()).equals(clazz.getEnclosingClass())) { // don't collect synth fields for inner classes
+                    collector.accept(name, type, annotations, deserializer, serializer);
+                }
+            });
+            if (instanceFactoryDelegate instanceof FieldDiscoverer.MutableInstanceFactory<Map<Field, Object>> mutableInstanceFactoryDelegate) {
+                return new MutableInstanceFactory<>() {
+                    @Override
+                    public Map<Field, Object> begin() {
+                        return mutableInstanceFactoryDelegate.begin();
+                    }
+
+                    @SuppressWarnings("unchecked")
+                    @Override
+                    public void complete(Object instance, Map<Field, Object> intermediate) throws SerializationException {
+                        final Iterator<Map.Entry<Field, Object>> iter = intermediate.entrySet().iterator();
+                        try {
+                            while (iter.hasNext()) { // manually merge any mergeable maps
+                                Map.Entry<Field, Object> entry = iter.next();
+                                if (entry.getKey().isAnnotationPresent(MergeMap.class) && Map.class.isAssignableFrom(entry.getKey().getType()) && intermediate.get(entry.getKey()) instanceof Map<?, ?> map) {
+                                    iter.remove();
+                                    @Nullable Map<Object, Object> existingMap = (Map<Object, Object>) entry.getKey().get(instance);
+                                    if (existingMap != null) {
+                                        existingMap.putAll(map);
+                                    } else {
+                                        entry.getKey().set(instance, entry.getValue());
+                                    }
+                                }
+                            }
+                        } catch (final IllegalAccessException e) {
+                            throw new SerializationException(target.getType(), e);
+                        }
+                        mutableInstanceFactoryDelegate.complete(instance, intermediate);
+                    }
+
+                    @Override
+                    public Object complete(Map<Field, Object> intermediate) throws SerializationException {
+                        @Nullable Object targetInstance = InnerClassFieldDiscoverer.this.instanceMap.get(GenericTypeReflector.erase(target.getType()));
+                        if (targetInstance != null) {
+                            this.complete(targetInstance, intermediate);
+                        } else {
+                            targetInstance = mutableInstanceFactoryDelegate.complete(intermediate);
+                        }
+                        if (targetInstance instanceof ConfigurationPart.Post post) {
+                            post.postProcess();
+                        }
+                        return targetInstance;
+                    }
+
+                    @Override
+                    public boolean canCreateInstances() {
+                        return mutableInstanceFactoryDelegate.canCreateInstances();
+                    }
+                };
+            }
+        }
+        return null;
+    }
+
+    private Map<Class<?>, Object> overrides() {
+        return this.overrides;
+    }
+
+    static FieldDiscoverer<?> worldConfig(Configurations.ContextMap contextMap) {
+        final Map<Class<?>, Object> overrides = Map.of(
+            WorldConfiguration.class, new WorldConfiguration(
+                contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(),
+                contextMap.require(Configurations.WORLD_KEY)
+            )
+        );
+        return new InnerClassFieldDiscoverer(overrides);
+    }
+
+    static FieldDiscoverer<?> globalConfig() {
+        return new InnerClassFieldDiscoverer(Collections.emptyMap());
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/MergeMap.java b/src/main/java/io/papermc/paper/configuration/MergeMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..a977b80cb196b7345bdfcb0b65ee2021f112efd1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/MergeMap.java
@@ -0,0 +1,19 @@
+package io.papermc.paper.configuration;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * For use in maps inside {@link ConfigurationPart}s that have default keys that shouldn't be removed by users
+ * <p>
+ * Note that when the config is reloaded, the maps will be merged again, so make sure this map can't accumulate
+ * keys overtime.
+ */
+@Documented
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MergeMap {
+}
diff --git a/src/main/java/io/papermc/paper/configuration/NestedSetting.java b/src/main/java/io/papermc/paper/configuration/NestedSetting.java
new file mode 100644
index 0000000000000000000000000000000000000000..69add4a7f1147015806bc9b63a8340d1893356c1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/NestedSetting.java
@@ -0,0 +1,32 @@
+package io.papermc.paper.configuration;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AnnotatedElement;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface NestedSetting {
+
+    String[] value();
+
+    class Factory implements NodeResolver.Factory {
+        @Override
+        public @Nullable NodeResolver make(String name, AnnotatedElement element) {
+            if (element.isAnnotationPresent(NestedSetting.class)) {
+                Object[] path = element.getAnnotation(NestedSetting.class).value();
+                if (path.length > 0) {
+                    return node -> node.node(path);
+                }
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2e961bbd33c6ecb7f049365b7aff6c5caa262ff
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
@@ -0,0 +1,431 @@
+package io.papermc.paper.configuration;
+
+import com.google.common.base.Suppliers;
+import com.google.common.collect.Table;
+import com.mojang.logging.LogUtils;
+import io.leangen.geantyref.TypeToken;
+import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization;
+import io.papermc.paper.configuration.serializer.ComponentSerializer;
+import io.papermc.paper.configuration.serializer.EnumValueSerializer;
+import io.papermc.paper.configuration.serializer.FastutilMapSerializer;
+import io.papermc.paper.configuration.serializer.PacketClassSerializer;
+import io.papermc.paper.configuration.serializer.StringRepresentableSerializer;
+import io.papermc.paper.configuration.serializer.TableSerializer;
+import io.papermc.paper.configuration.serializer.collections.MapSerializer;
+import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer;
+import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer;
+import io.papermc.paper.configuration.transformation.Transformations;
+import io.papermc.paper.configuration.transformation.global.LegacyPaperConfig;
+import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration;
+import io.papermc.paper.configuration.transformation.world.LegacyPaperWorldConfig;
+import io.papermc.paper.configuration.type.BooleanOrDefault;
+import io.papermc.paper.configuration.type.DoubleOrDefault;
+import io.papermc.paper.configuration.type.Duration;
+import io.papermc.paper.configuration.type.EngineMode;
+import io.papermc.paper.configuration.type.IntOrDefault;
+import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.bukkit.command.Command;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.spigotmc.SpigotConfig;
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.BasicConfigurationNode;
+import org.spongepowered.configurate.ConfigurateException;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.ConfigurationOptions;
+import org.spongepowered.configurate.NodePath;
+import org.spongepowered.configurate.objectmapping.ObjectMapper;
+import org.spongepowered.configurate.transformation.ConfigurationTransformation;
+import org.spongepowered.configurate.transformation.TransformAction;
+import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static com.google.common.base.Preconditions.checkState;
+import static io.leangen.geantyref.GenericTypeReflector.erase;
+
+@SuppressWarnings("Convert2Diamond")
+public class PaperConfigurations extends Configurations<GlobalConfiguration, WorldConfiguration> {
+
+    private static final Logger LOGGER = LogUtils.getLogger();
+    static final String GLOBAL_CONFIG_FILE_NAME = "paper-global.yml";
+    static final String WORLD_DEFAULTS_CONFIG_FILE_NAME = "paper-world-defaults.yml";
+    static final String WORLD_CONFIG_FILE_NAME = "paper-world.yml";
+    public static final String CONFIG_DIR = "config";
+    private static final String BACKUP_DIR ="legacy-backup";
+
+    private static final String GLOBAL_HEADER = String.format("""
+            This is the global configuration file for Paper.
+            As you can see, there's a lot to configure. Some options may impact gameplay, so use
+            with caution, and make sure you know what each option does before configuring.
+
+            If you need help with the configuration or have any questions related to Paper,
+            join us in our Discord or check the docs page.
+
+            The world configuration options have been moved inside
+            their respective world folder. The files are named %s
+
+            Docs: https://docs.papermc.io/
+            Discord: https://discord.gg/papermc
+            Website: https://papermc.io/""", WORLD_CONFIG_FILE_NAME);
+
+    private static final String WORLD_DEFAULTS_HEADER = """
+            This is the world defaults configuration file for Paper.
+            As you can see, there's a lot to configure. Some options may impact gameplay, so use
+            with caution, and make sure you know what each option does before configuring.
+
+            If you need help with the configuration or have any questions related to Paper,
+            join us in our Discord or check the docs page.
+
+            Configuration options here apply to all worlds, unless you specify overrides inside
+            the world-specific config file inside each world folder.
+
+            Docs: https://docs.papermc.io/
+            Discord: https://discord.gg/papermc
+            Website: https://papermc.io/""";
+
+    private static final Function<ContextMap, String> WORLD_HEADER = map -> String.format("""
+        This is a world configuration file for Paper.
+        This file may start empty but can be filled with settings to override ones in the %s/%s
+        
+        World: %s (%s)""",
+        PaperConfigurations.CONFIG_DIR,
+        PaperConfigurations.WORLD_DEFAULTS_CONFIG_FILE_NAME,
+        map.require(WORLD_NAME),
+        map.require(WORLD_KEY)
+    );
+
+    private static final String MOVED_NOTICE = """
+        The global and world default configuration files have moved to %s
+        and the world-specific configuration file has been moved inside
+        the respective world folder.
+        
+        See https://docs.papermc.io/paper/configuration for more information.
+        """;
+
+    private static final Supplier<SpigotWorldConfig> SPIGOT_WORLD_DEFAULTS = Suppliers.memoize(() -> new SpigotWorldConfig(RandomStringUtils.randomAlphabetic(255)) {
+        @Override // override to ensure "verbose" is false
+        public void init() {
+            SpigotConfig.readConfig(SpigotWorldConfig.class, this);
+        }
+    });
+    static final ContextKey<Supplier<SpigotWorldConfig>> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken<Supplier<SpigotWorldConfig>>() {}, "spigot world config");
+
+
+    public PaperConfigurations(final Path globalFolder) {
+        super(globalFolder, GlobalConfiguration.class, WorldConfiguration.class, GLOBAL_CONFIG_FILE_NAME, WORLD_DEFAULTS_CONFIG_FILE_NAME, WORLD_CONFIG_FILE_NAME);
+    }
+
+    @Override
+    protected YamlConfigurationLoader.Builder createLoaderBuilder() {
+        return super.createLoaderBuilder()
+            .defaultOptions(PaperConfigurations::defaultOptions);
+    }
+
+    private static ConfigurationOptions defaultOptions(ConfigurationOptions options) {
+        return options.serializers(builder -> builder
+            .register(MapSerializer.TYPE, new MapSerializer(false))
+            .register(new EnumValueSerializer())
+            .register(new ComponentSerializer())
+        );
+    }
+
+    @Override
+    protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() {
+        return defaultGlobalFactoryBuilder(super.createGlobalObjectMapperFactoryBuilder());
+    }
+
+    private static ObjectMapper.Factory.Builder defaultGlobalFactoryBuilder(ObjectMapper.Factory.Builder builder) {
+        return builder.addDiscoverer(InnerClassFieldDiscoverer.globalConfig());
+    }
+
+    @Override
+    protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() {
+        return super.createGlobalLoaderBuilder()
+            .defaultOptions(PaperConfigurations::defaultGlobalOptions);
+    }
+
+    private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options) {
+        return options
+            .header(GLOBAL_HEADER)
+            .serializers(builder -> builder.register(new PacketClassSerializer()));
+    }
+
+    @Override
+    public GlobalConfiguration initializeGlobalConfiguration() throws ConfigurateException {
+        GlobalConfiguration configuration = super.initializeGlobalConfiguration();
+        GlobalConfiguration.set(configuration);
+        return configuration;
+    }
+
+    @Override
+    protected ContextMap.Builder createDefaultContextMap() {
+        return super.createDefaultContextMap()
+            .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, SPIGOT_WORLD_DEFAULTS);
+    }
+
+    @Override
+    protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) {
+        return super.createWorldObjectMapperFactoryBuilder(contextMap)
+            .addNodeResolver(new RequiresSpigotInitialization.Factory(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get()))
+            .addNodeResolver(new NestedSetting.Factory())
+            .addDiscoverer(InnerClassFieldDiscoverer.worldConfig(contextMap));
+    }
+
+    @Override
+    protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) {
+        return super.createWorldConfigLoaderBuilder(contextMap)
+            .defaultOptions(options -> options
+                .header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? WORLD_DEFAULTS_HEADER : WORLD_HEADER.apply(contextMap))
+                .serializers(serializers -> serializers
+                    .register(new TypeToken<Reference2IntMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2IntMap<?>>(Reference2IntOpenHashMap::new, Integer.TYPE))
+                    .register(new TypeToken<Reference2LongMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2LongMap<?>>(Reference2LongOpenHashMap::new, Long.TYPE))
+                    .register(new TypeToken<Table<?, ?, ?>>() {}, new TableSerializer())
+                    .register(new StringRepresentableSerializer())
+                    .register(IntOrDefault.SERIALIZER)
+                    .register(DoubleOrDefault.SERIALIZER)
+                    .register(BooleanOrDefault.SERIALIZER)
+                    .register(Duration.SERIALIZER)
+                    .register(EngineMode.SERIALIZER)
+                    .register(FallbackValueSerializer.create(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), MinecraftServer::getServer))
+                    .register(new RegistryValueSerializer<>(new TypeToken<EntityType<?>>() {}, Registry.ENTITY_TYPE_REGISTRY, true))
+                    .register(new RegistryValueSerializer<>(Item.class, Registry.ITEM_REGISTRY, true))
+                    .register(new RegistryHolderSerializer<>(new TypeToken<ConfiguredFeature<?, ?>>() {}, Registry.CONFIGURED_FEATURE_REGISTRY, false))
+                    .register(new RegistryHolderSerializer<>(Item.class, Registry.ITEM_REGISTRY, true))
+                )
+            );
+    }
+
+    @Override
+    protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException {
+        final ConfigurationNode version = node.node(Configuration.VERSION_FIELD);
+        final String world = contextMap.require(WORLD_NAME);
+        if (version.virtual()) {
+            LOGGER.warn("The world config file for " + world + " didn't have a version set, assuming latest");
+            version.raw(WorldConfiguration.CURRENT_VERSION);
+        }
+        ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
+        for (NodePath path : RemovedConfigurations.REMOVED_WORLD_PATHS) {
+            builder.addAction(path, TransformAction.remove());
+        }
+        builder.build().apply(node);
+        // ADD FUTURE TRANSFORMS HERE
+    }
+
+    @Override
+    protected void applyGlobalConfigTransformations(ConfigurationNode node) throws ConfigurateException {
+        ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
+        for (NodePath path : RemovedConfigurations.REMOVED_GLOBAL_PATHS) {
+            builder.addAction(path, TransformAction.remove());
+        }
+        builder.build().apply(node);
+        // ADD FUTURE TRANSFORMS HERE
+    }
+
+    private static final List<Transformations.DefaultsAware> DEFAULT_AWARE_TRANSFORMATIONS = List.of(FeatureSeedsGeneration::apply);
+
+    @Override
+    protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException {
+        final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
+        // ADD FUTURE TRANSFORMS HERE (these transforms run after the defaults have been merged into the node)
+        DEFAULT_AWARE_TRANSFORMATIONS.forEach(transform -> transform.apply(builder, contextMap, defaultsNode));
+
+        ConfigurationTransformation transformation;
+        try {
+            transformation = builder.build(); // build throws IAE if no actions were provided (bad zml)
+        } catch (IllegalArgumentException ignored) {
+            return;
+        }
+        transformation.apply(worldNode);
+    }
+
+    @Override
+    public WorldConfiguration createWorldConfig(final ContextMap contextMap) {
+        final String levelName = contextMap.require(WORLD_NAME);
+        try {
+            return super.createWorldConfig(contextMap);
+        } catch (IOException exception) {
+            throw new RuntimeException("Could not create world config for " + levelName, exception);
+        }
+    }
+
+    @Override
+    protected boolean isConfigType(final Type type) {
+        return ConfigurationPart.class.isAssignableFrom(erase(type));
+    }
+
+    public void reloadConfigs(MinecraftServer server) {
+        try {
+            this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get()));
+            this.initializeWorldDefaultsConfiguration();
+            for (ServerLevel level : server.getAllLevels()) {
+                this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig()));
+            }
+        } catch (Exception ex) {
+            throw new RuntimeException("Could not reload paper configuration files", ex);
+        }
+    }
+
+    private static ContextMap createWorldContextMap(ServerLevel level) {
+        return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig);
+    }
+
+    public static ContextMap createWorldContextMap(Path dir, String levelName, ResourceLocation worldKey, SpigotWorldConfig spigotConfig) {
+        return ContextMap.builder()
+            .put(WORLD_DIRECTORY, dir)
+            .put(WORLD_NAME, levelName)
+            .put(WORLD_KEY, worldKey)
+            .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, Suppliers.ofInstance(spigotConfig))
+            .build();
+    }
+
+    public static PaperConfigurations setup(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception {
+        final Path legacy = Files.isSymbolicLink(legacyConfig) ? Files.readSymbolicLink(legacyConfig) : legacyConfig;
+        final Path replacementFile = legacy.resolveSibling(legacyConfig.getFileName() + "-README.txt");
+        if (Files.notExists(replacementFile)) {
+            Files.createFile(replacementFile);
+            Files.writeString(replacementFile, String.format(MOVED_NOTICE, configDir.toAbsolutePath()));
+        }
+        if (needsConverting(legacyConfig)) {
+            try {
+                if (Files.exists(configDir) && !Files.isDirectory(configDir)) {
+                    throw new RuntimeException("Paper needs to create a '" + CONFIG_DIR + "' folder in the root of your server. You already have a non-directory named '" + CONFIG_DIR + "'. Please remove it and restart the server.");
+                }
+                final Path backupDir = configDir.resolve(BACKUP_DIR);
+                if (Files.exists(backupDir) && !Files.isDirectory(backupDir)) {
+                    throw new RuntimeException("Paper needs to create a '" + BACKUP_DIR + "' directory in the '" + CONFIG_DIR + "' folder. You already have a non-directory named '" + BACKUP_DIR + "'. Please remove it and restart the server.");
+                }
+                createDirectoriesSymlinkAware(backupDir);
+                final String backupFileName = legacyConfig.getFileName().toString() + ".old";
+                final Path legacyConfigBackup = backupDir.resolve(backupFileName);
+                if (Files.exists(legacyConfigBackup) && !Files.isRegularFile(legacyConfigBackup)) {
+                    throw new RuntimeException("Paper needs to create a '" + backupFileName + "' file in the '" + BACKUP_DIR + "' folder. You already have a non-file named '" + backupFileName + "'. Please remove it and restart the server.");
+                }
+                Files.move(legacyConfig.toRealPath(), legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup
+                if (Files.isSymbolicLink(legacyConfig)) {
+                    Files.delete(legacyConfig);
+                }
+                convert(legacyConfigBackup, configDir, worldFolder, spigotConfig);
+            } catch (final IOException ex) {
+                throw new RuntimeException("Could not convert '" + legacyConfig.getFileName().toString() + "' to the new configuration format", ex);
+            }
+        }
+        try {
+            createDirectoriesSymlinkAware(configDir);
+            return new PaperConfigurations(configDir);
+        } catch (final IOException ex) {
+            throw new RuntimeException("Could not setup PaperConfigurations", ex);
+        }
+    }
+
+    private static void convert(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception {
+        createDirectoriesSymlinkAware(configDir);
+
+        final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig);
+        final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME));
+        final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME));
+
+        final ConfigurationNode legacy = legacyLoader.load();
+        checkState(!legacy.virtual(), "can't be virtual");
+        final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt();
+
+        final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy();
+        checkState(!legacyWorldSettings.virtual(), "can't be virtual");
+        legacy.removeChild("world-settings");
+
+        // Apply legacy transformations before settings flatten
+        final YamlConfiguration spigotConfiguration = loadLegacyConfigFile(spigotConfig); // needs to change spigot config values in this transformation
+        LegacyPaperConfig.transformation(spigotConfiguration).apply(legacy);
+        spigotConfiguration.save(spigotConfig);
+        legacy.mergeFrom(legacy.node("settings")); // flatten "settings" to root
+        legacy.removeChild("settings");
+        LegacyPaperConfig.toNewFormat().apply(legacy);
+        globalLoader.save(legacy); // save converted node to new global location
+
+        final ConfigurationNode worldDefaults = legacyWorldSettings.node("default").copy();
+        checkState(!worldDefaults.virtual());
+        worldDefaults.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
+        legacyWorldSettings.removeChild("default");
+        LegacyPaperWorldConfig.transformation().apply(worldDefaults);
+        LegacyPaperWorldConfig.toNewFormat().apply(worldDefaults);
+        worldDefaultsLoader.save(worldDefaults);
+
+        legacyWorldSettings.childrenMap().forEach((world, legacyWorldNode) -> {
+            try {
+                legacyWorldNode.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
+                LegacyPaperWorldConfig.transformation().apply(legacyWorldNode);
+                LegacyPaperWorldConfig.toNewFormat().apply(legacyWorldNode);
+                ConfigurationLoaders.naturallySortedWithoutHeader(worldFolder.resolve(world.toString()).resolve(WORLD_CONFIG_FILE_NAME)).save(legacyWorldNode); // save converted node to new location
+            } catch (final ConfigurateException ex) {
+                ex.printStackTrace();
+            }
+        });
+    }
+
+    private static boolean needsConverting(final Path legacyConfig) {
+        return Files.exists(legacyConfig) && Files.isRegularFile(legacyConfig);
+    }
+
+    @Deprecated
+    public YamlConfiguration createLegacyObject(final MinecraftServer server) {
+        YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile());
+        ConfigurationSection worlds = global.createSection("__________WORLDS__________");
+        worlds.set("__defaults__", YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile()));
+        for (ServerLevel level : server.getAllLevels()) {
+            worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile()));
+        }
+        return global;
+    }
+
+    @Deprecated
+    public static YamlConfiguration loadLegacyConfigFile(File configFile) throws Exception {
+        YamlConfiguration config = new YamlConfiguration();
+        if (configFile.exists()) {
+            try {
+                config.load(configFile);
+            } catch (Exception ex) {
+                throw new Exception("Failed to load configuration file: " + configFile.getName(), ex);
+            }
+        }
+        return config;
+    }
+
+    @VisibleForTesting
+    static ConfigurationNode createForTesting() {
+        ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build();
+        ConfigurationOptions options = defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults()))
+            .serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer()));
+        return BasicConfigurationNode.root(options);
+    }
+
+    // Symlinks are not correctly checked in createDirectories
+    static void createDirectoriesSymlinkAware(Path path) throws IOException {
+        if (!Files.isDirectory(path)) {
+            Files.createDirectories(path);
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java
new file mode 100644
index 0000000000000000000000000000000000000000..1bb16fc7598cd53e822d84b69d6a9727b37f484f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java
@@ -0,0 +1,63 @@
+package io.papermc.paper.configuration;
+
+import org.spongepowered.configurate.NodePath;
+
+import static org.spongepowered.configurate.NodePath.path;
+
+interface RemovedConfigurations {
+
+    NodePath[] REMOVED_WORLD_PATHS = {
+        path("elytra-hit-wall-damage"),
+        path("queue-light-updates"),
+        path("save-queue-limit-for-auto-save"),
+        path("max-chunk-sends-per-tick"),
+        path("max-chunk-gens-per-tick"),
+        path("fire-physics-event-for-redstone"),
+        path("fix-zero-tick-instant-grow-farms"),
+        path("bed-search-radius"),
+        path("lightning-strike-distance-limit"),
+        path("fix-wither-targeting-bug"),
+        path("remove-corrupt-tile-entities"),
+        path("allow-undead-horse-leashing"),
+        path("reset-arrow-despawn-timer-on-fall"),
+        path("seed-based-feature-search"),
+        path("seed-based-feature-search-loads-chunks"),
+        path("viewdistances.no-tick-view-distance"),
+        path("seed-based-feature-search"), // unneeded as of 1.18
+        path("seed-based-feature-search-loads-chunks"), // unneeded as of 1.18
+        path("reset-arrow-despawn-timer-on-fall"),
+        path("squid-spawn-height"),
+        path("viewdistances"),
+        path("use-alternate-fallingblock-onGround-detection"),
+        path("skip-entity-ticking-in-chunks-scheduled-for-unload"),
+        path("tracker-update-distance"),
+        path("allow-block-location-tab-completion"),
+        path("cache-chunk-maps"),
+        path("disable-mood-sounds"),
+        path("fix-cannons"),
+        path("player-blocking-damage-multiplier"),
+        path("remove-invalid-mob-spawner-tile-entities"),
+        path("use-hopper-check"),
+        path("use-async-lighting"),
+        path("tnt-explosion-volume"),
+        path("entities", "spawning", "despawn-ranges", "soft"),
+        path("entities", "spawning", "despawn-ranges", "hard")
+    };
+
+    NodePath[] REMOVED_GLOBAL_PATHS = {
+        path("queue-light-updates-max-loss"),
+        path("sleep-between-chunk-saves"),
+        path("remove-invalid-statistics"),
+        path("min-chunk-load-threads"),
+        path("use-versioned-world"),
+        path("save-player-data"), // to spigot (converted)
+        path("log-named-entity-deaths"), // default in vanilla
+        path("chunk-tasks-per-tick"), // removed in tuinity merge
+        path("item-validation", "loc-name"),
+        path("commandErrorMessage"),
+        path("baby-zombie-movement-speed"),
+        path("limit-player-interactions"),
+        path("warnWhenSettingExcessiveVelocity")
+    };
+
+}
diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..e2c612dd55fcb2769fb06f7878b8d0873f2be139
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
@@ -0,0 +1,467 @@
+package io.papermc.paper.configuration;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.configuration.constraint.Constraint;
+import io.papermc.paper.configuration.constraint.Constraints;
+import io.papermc.paper.configuration.legacy.MaxEntityCollisionsInitializer;
+import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization;
+import io.papermc.paper.configuration.legacy.SpawnLoadedRangeInitializer;
+import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration;
+import io.papermc.paper.configuration.type.BooleanOrDefault;
+import io.papermc.paper.configuration.type.DoubleOrDefault;
+import io.papermc.paper.configuration.type.Duration;
+import io.papermc.paper.configuration.type.EngineMode;
+import io.papermc.paper.configuration.type.IntOrDefault;
+import io.papermc.paper.configuration.type.fallback.ArrowDespawnRate;
+import io.papermc.paper.configuration.type.fallback.AutosavePeriod;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
+import net.minecraft.Util;
+import net.minecraft.core.Holder;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.Difficulty;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.MobCategory;
+import net.minecraft.world.entity.monster.Vindicator;
+import net.minecraft.world.entity.monster.Zombie;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.NaturalSpawner;
+import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
+import org.slf4j.Logger;
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Required;
+import org.spongepowered.configurate.objectmapping.meta.Setting;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
+public class WorldConfiguration extends ConfigurationPart {
+    private static final Logger LOGGER = LogUtils.getLogger();
+    static final int CURRENT_VERSION = 28;
+
+    private transient final SpigotWorldConfig spigotConfig;
+    private transient final ResourceLocation worldKey;
+    WorldConfiguration(SpigotWorldConfig spigotConfig, ResourceLocation worldKey) {
+        this.spigotConfig = spigotConfig;
+        this.worldKey = worldKey;
+    }
+
+    public boolean isDefault() {
+        return this.worldKey.equals(PaperConfigurations.WORLD_DEFAULTS_KEY);
+    }
+
+    @Setting(Configuration.VERSION_FIELD)
+    public int version = CURRENT_VERSION;
+
+    public Anticheat anticheat;
+
+    public class Anticheat extends ConfigurationPart {
+
+        public Obfuscation obfuscation;
+
+        public class Obfuscation extends ConfigurationPart {
+            public Items items = new Items();
+            public class Items extends ConfigurationPart {
+                public boolean hideItemmeta = false;
+                public boolean hideDurability = false;
+            }
+        }
+
+        public AntiXray antiXray;
+
+        public class AntiXray extends ConfigurationPart {
+            public boolean enabled = false;
+            public EngineMode engineMode = EngineMode.HIDE;
+            public int maxBlockHeight = 64;
+            public int updateRadius = 2;
+            public boolean lavaObscures = false;
+            public boolean usePermission = false;
+            public List<String> hiddenBlocks = List.of("copper_ore", "deepslate_copper_ore", "gold_ore", "deepslate_gold_ore", "iron_ore", "deepslate_iron_ore",
+                "coal_ore", "deepslate_coal_ore", "lapis_ore", "deepslate_lapis_ore", "mossy_cobblestone", "obsidian", "chest", "diamond_ore", "deepslate_diamond_ore",
+                "redstone_ore", "deepslate_redstone_ore", "clay", "emerald_ore", "deepslate_emerald_ore", "ender_chest"); // TODO update type to List<Block>
+            public List<String> replacementBlocks = List.of("stone", "oak_planks", "deepslate"); // TODO update type to List<Block>
+        }
+    }
+
+    public Entities entities;
+
+    public class Entities extends ConfigurationPart {
+        public boolean entitiesTargetWithFollowRange = false;
+        public MobEffects mobEffects;
+
+        public class MobEffects extends ConfigurationPart {
+            public boolean undeadImmuneToCertainEffects = true;
+            public boolean spidersImmuneToPoisonEffect = true;
+            public ImmuneToWitherEffect immuneToWitherEffect;
+
+            public class ImmuneToWitherEffect extends ConfigurationPart {
+                public boolean wither = true;
+                public boolean witherSkeleton = true;
+            }
+        }
+
+        public ArmorStands armorStands;
+
+        public class ArmorStands extends ConfigurationPart {
+            public boolean doCollisionEntityLookups = true;
+            public boolean tick = true;
+        }
+
+        public Spawning spawning;
+
+        public class Spawning extends ConfigurationPart {
+            public ArrowDespawnRate nonPlayerArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig);
+            public ArrowDespawnRate creativeArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig);
+            public boolean filterNbtDataFromSpawnEggsAndRelated = true;
+            public boolean disableMobSpawnerSpawnEggTransformation = false;
+            public boolean perPlayerMobSpawns = true;
+            public boolean scanForLegacyEnderDragon = true;
+            @MergeMap
+            public Reference2IntMap<MobCategory> spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1)));
+            @MergeMap
+            public Map<MobCategory, DespawnRange> despawnRanges = Arrays.stream(MobCategory.values()).collect(Collectors.toMap(Function.identity(), category -> new DespawnRange(category.getNoDespawnDistance(), category.getDespawnDistance())));
+
+            @ConfigSerializable
+            public record DespawnRange(@Required int soft, @Required int hard) {
+            }
+
+            public WaterAnimalSpawnHeight wateranimalSpawnHeight;
+
+            public class WaterAnimalSpawnHeight extends ConfigurationPart {
+                public IntOrDefault maximum = IntOrDefault.USE_DEFAULT;
+                public IntOrDefault minimum = IntOrDefault.USE_DEFAULT;
+            }
+
+            public SlimeSpawnHeight slimeSpawnHeight;
+
+            public class SlimeSpawnHeight extends ConfigurationPart {
+
+                public SurfaceSpawnableSlimeBiome surfaceBiome;
+
+                public class SurfaceSpawnableSlimeBiome extends ConfigurationPart {
+                    public double maximum = 70;
+                    public double minimum = 50;
+                }
+
+                public SlimeChunk slimeChunk;
+
+                public class SlimeChunk extends ConfigurationPart {
+                    public double maximum = 40;
+                }
+            }
+
+            public WanderingTrader wanderingTrader;
+
+            public class WanderingTrader extends ConfigurationPart {
+                public int spawnMinuteLength = 1200;
+                public int spawnDayLength = 24000;
+                public int spawnChanceFailureIncrement = 25;
+                public int spawnChanceMin = 25;
+                public int spawnChanceMax = 75;
+            }
+
+            public boolean allChunksAreSlimeChunks = false;
+            @Constraint(Constraints.BelowZeroDoubleToDefault.class)
+            public DoubleOrDefault skeletonHorseThunderSpawnChance = DoubleOrDefault.USE_DEFAULT;
+            public boolean ironGolemsCanSpawnInAir = false;
+            public boolean countAllMobsForSpawning = false;
+            public int monsterSpawnMaxLightLevel = -1;
+            public DuplicateUUID duplicateUuid;
+
+            public class DuplicateUUID extends ConfigurationPart {
+                public DuplicateUUIDMode mode = DuplicateUUIDMode.SAFE_REGEN;
+                public int safeRegenDeleteRange = 32;
+
+                public enum DuplicateUUIDMode {
+                    SAFE_REGEN, DELETE, NOTHING, WARN;
+                }
+            }
+            public AltItemDespawnRate altItemDespawnRate;
+
+            public class AltItemDespawnRate extends ConfigurationPart {
+                public boolean enabled = false;
+                public Reference2IntMap<Item> items = new Reference2IntOpenHashMap<>(Map.of(Items.COBBLESTONE, 300));
+            }
+        }
+
+        public Behavior behavior;
+
+        public class Behavior extends ConfigurationPart {
+            public boolean disableChestCatDetection = false;
+            public boolean spawnerNerfedMobsShouldJump = false;
+            public int experienceMergeMaxValue = -1;
+            public boolean shouldRemoveDragon = false;
+            public boolean zombiesTargetTurtleEggs = true;
+            public boolean piglinsGuardChests = true;
+            public double babyZombieMovementModifier = 0.5;
+            public DoorBreakingDifficulty doorBreakingDifficulty;
+
+            public class DoorBreakingDifficulty extends ConfigurationPart { // TODO convert to map at some point
+                public List<Difficulty> zombie = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList();
+                public List<Difficulty> husk = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList();
+                @Setting("zombie_villager")
+                public List<Difficulty> zombieVillager = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList();
+                @Setting("zombified_piglin")
+                public List<Difficulty> zombified_piglin = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList();
+                public List<Difficulty> vindicator = Arrays.stream(Difficulty.values()).filter(Vindicator.DOOR_BREAKING_PREDICATE).toList();
+
+                // TODO remove when this becomes a proper map
+                public List<Difficulty> get(EntityType<?> type) {
+                    return this.getOrDefault(type, null);
+                }
+
+                public List<Difficulty> getOrDefault(EntityType<?> type, List<Difficulty> fallback) {
+                    if (type == EntityType.ZOMBIE) {
+                        return this.zombie;
+                    } else if (type == EntityType.HUSK) {
+                        return this.husk;
+                    } else if (type == EntityType.ZOMBIE_VILLAGER) {
+                        return this.zombieVillager;
+                    } else if (type == EntityType.ZOMBIFIED_PIGLIN) {
+                        return this.zombified_piglin;
+                    } else if (type == EntityType.VINDICATOR) {
+                        return this.vindicator;
+                    } else {
+                        return fallback;
+                    }
+                }
+            }
+
+            public boolean disableCreeperLingeringEffect = false;
+            public boolean enderDragonsDeathAlwaysPlacesDragonEgg = false;
+            public boolean phantomsDoNotSpawnOnCreativePlayers = true;
+            public boolean phantomsOnlyAttackInsomniacs = true;
+            public boolean parrotsAreUnaffectedByPlayerMovement = false;
+            public double zombieVillagerInfectionChance = -1.0;
+            public MobsCanAlwaysPickUpLoot mobsCanAlwaysPickUpLoot;
+
+            public class MobsCanAlwaysPickUpLoot extends ConfigurationPart {
+                public boolean zombies = false;
+                public boolean skeletons = false;
+            }
+
+            public boolean disablePlayerCrits = false;
+            public boolean nerfPigmenFromNetherPortals = false;
+            public PillagerPatrols pillagerPatrols;
+
+            public class PillagerPatrols extends ConfigurationPart {
+                public boolean disable = false;
+                public double spawnChance = 0.2;
+                public SpawnDelay spawnDelay;
+                public Start start;
+
+                public class SpawnDelay extends ConfigurationPart {
+                    public boolean perPlayer = false;
+                    public int ticks = 12000;
+                }
+
+                public class Start extends ConfigurationPart {
+                    public boolean perPlayer = false;
+                    public int day = 5;
+                }
+            }
+        }
+    }
+
+    public Lootables lootables;
+
+    public class Lootables extends ConfigurationPart {
+        public boolean autoReplenish = false;
+        public boolean restrictPlayerReloot = true;
+        public boolean resetSeedOnFill = true;
+        public int maxRefills = -1;
+        public Duration refreshMin = Duration.of("12h");
+        public Duration refreshMax = Duration.of("2d");
+    }
+
+    public MaxGrowthHeight maxGrowthHeight;
+
+    public class MaxGrowthHeight extends ConfigurationPart {
+        public int cactus = 3;
+        public int reeds = 3;
+        public Bamboo bamboo;
+
+        public class Bamboo extends ConfigurationPart {
+            public int max = 16;
+            public int min = 11;
+        }
+    }
+
+    public Scoreboards scoreboards;
+
+    public class Scoreboards extends ConfigurationPart {
+        public boolean allowNonPlayerEntitiesOnScoreboards = false;
+        public boolean useVanillaWorldScoreboardNameColoring = false;
+    }
+
+    public Environment environment;
+
+    public class Environment extends ConfigurationPart {
+        public boolean disableThunder = false;
+        public boolean disableIceAndSnow = false;
+        public boolean optimizeExplosions = false;
+        public boolean disableExplosionKnockback = false;
+        public boolean generateFlatBedrock = false;
+        public FrostedIce frostedIce;
+
+        public class FrostedIce extends ConfigurationPart {
+            public boolean enabled = true;
+            public Delay delay;
+
+            public class Delay extends ConfigurationPart {
+                public int min = 20;
+                public int max = 40;
+            }
+        }
+
+        public TreasureMaps treasureMaps;
+        public class TreasureMaps extends ConfigurationPart {
+            public boolean enabled = true;
+            @NestedSetting({"find-already-discovered", "villager-trade"})
+            public boolean findAlreadyDiscoveredVillager = false;
+            @NestedSetting({"find-already-discovered", "loot-tables"})
+            public BooleanOrDefault findAlreadyDiscoveredLootTable = BooleanOrDefault.USE_DEFAULT;
+        }
+
+        public int waterOverLavaFlowSpeed = 5;
+        public int portalSearchRadius = 128;
+        public int portalCreateRadius = 16;
+        public boolean portalSearchVanillaDimensionScaling = true;
+        public boolean disableTeleportationSuffocationCheck = false;
+        public int netherCeilingVoidDamageHeight = 0;
+    }
+
+    public Spawn spawn;
+
+    public class Spawn extends ConfigurationPart {
+        @RequiresSpigotInitialization(SpawnLoadedRangeInitializer.class)
+        public short keepSpawnLoadedRange = 10;
+        public boolean keepSpawnLoaded = true;
+        public boolean allowUsingSignsInsideSpawnProtection = false;
+    }
+
+    public Maps maps;
+
+    public class Maps extends ConfigurationPart {
+        public int itemFrameCursorLimit = 128;
+        public int itemFrameCursorUpdateInterval = 10;
+    }
+
+    public Fixes fixes;
+
+    public class Fixes extends ConfigurationPart {
+        public boolean fixItemsMergingThroughWalls = false;
+        public boolean disableUnloadedChunkEnderpearlExploit = true;
+        public boolean preventTntFromMovingInWater = false;
+        public boolean splitOverstackedLoot = true;
+        public boolean fixCuringZombieVillagerDiscountExploit = true;
+        public int fallingBlockHeightNerf = 0;
+        public int tntEntityHeightNerf = 0;
+    }
+
+    public UnsupportedSettings unsupportedSettings;
+
+    public class UnsupportedSettings extends ConfigurationPart {
+        public boolean fixInvulnerableEndCrystalExploit = true;
+    }
+
+    public Hopper hopper;
+
+    public class Hopper extends ConfigurationPart {
+        public boolean cooldownWhenFull = true;
+        public boolean disableMoveEvent = false;
+        public boolean ignoreOccludingBlocks = false;
+    }
+
+    public Collisions collisions;
+
+    public class Collisions extends ConfigurationPart {
+        public boolean onlyPlayersCollide = false;
+        public boolean allowVehicleCollisions = true;
+        public boolean fixClimbingBypassingCrammingRule = false;
+        @RequiresSpigotInitialization(MaxEntityCollisionsInitializer.class)
+        public int maxEntityCollisions = 8;
+        public boolean allowPlayerCrammingDamage = false;
+    }
+
+    public Chunks chunks;
+
+    public class Chunks extends ConfigurationPart {
+        public AutosavePeriod autoSaveInterval = AutosavePeriod.def();
+        public int maxAutoSaveChunksPerTick = 24;
+        public int fixedChunkInhabitedTime = -1;
+        public boolean preventMovingIntoUnloadedChunks = false;
+        public Duration delayChunkUnloadsBy = Duration.of("10s");
+        public Reference2IntMap<EntityType<?>> entityPerChunkSaveLimit = Util.make(new Reference2IntOpenHashMap<>(Registry.ENTITY_TYPE.size()), map -> {
+            map.defaultReturnValue(-1);
+            map.put(EntityType.EXPERIENCE_ORB, -1);
+            map.put(EntityType.SNOWBALL, -1);
+            map.put(EntityType.ENDER_PEARL, -1);
+            map.put(EntityType.ARROW, -1);
+            map.put(EntityType.FIREBALL, -1);
+            map.put(EntityType.SMALL_FIREBALL, -1);
+        });
+    }
+
+    public FishingTimeRange fishingTimeRange;
+
+    public class FishingTimeRange extends ConfigurationPart {
+        public int minimum = 100;
+        public int maximum = 600;
+    }
+
+    public TickRates tickRates;
+
+    public class TickRates extends ConfigurationPart {
+        public int grassSpread = 1;
+        public int containerUpdate = 1;
+        public int mobSpawner = 1;
+        public Table<EntityType<?>, String, Integer> sensor = Util.make(HashBasedTable.create(), table -> table.put(EntityType.VILLAGER, "secondarypoisensor", 40));
+        public Table<EntityType<?>, String, Integer> behavior = Util.make(HashBasedTable.create(), table -> table.put(EntityType.VILLAGER, "validatenearbypoi", -1));
+    }
+
+    @Setting(FeatureSeedsGeneration.FEATURE_SEEDS_KEY)
+    public FeatureSeeds featureSeeds;
+
+    public class FeatureSeeds extends ConfigurationPart.Post {
+        @Setting(FeatureSeedsGeneration.GENERATE_KEY)
+        public boolean generateRandomSeedsForAll = false;
+        @Setting(FeatureSeedsGeneration.FEATURES_KEY)
+        public Reference2LongMap<Holder<ConfiguredFeature<?, ?>>> features = new Reference2LongOpenHashMap<>();
+
+        @Override
+        public void postProcess() {
+            this.features.defaultReturnValue(-1);
+        }
+    }
+
+    public Misc misc;
+
+    public class Misc extends ConfigurationPart {
+        public int lightQueueSize = 20;
+        public boolean updatePathfindingOnBlockUpdate = true;
+        public boolean showSignClickCommandFailureMsgsToPlayer = false;
+        public RedstoneImplementation redstoneImplementation = RedstoneImplementation.VANILLA;
+        public boolean disableEndCredits = false;
+        public float maxLeashDistance = 10f;
+        public boolean disableSprintInterruptionOnAttack = false;
+        public int shieldBlockingDelay = 5;
+        public boolean disableRelativeProjectileVelocity = false;
+
+        public enum RedstoneImplementation {
+            VANILLA, EIGENCRAFT, ALTERNATE_CURRENT
+        }
+    }
+
+}
diff --git a/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java b/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java
new file mode 100644
index 0000000000000000000000000000000000000000..514be9a11e2ca368ea72dd2bac1b84bff5468814
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java
@@ -0,0 +1,30 @@
+package io.papermc.paper.configuration.constraint;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Type;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
+public @interface Constraint {
+    Class<? extends org.spongepowered.configurate.objectmapping.meta.Constraint<?>> value();
+
+    class Factory implements org.spongepowered.configurate.objectmapping.meta.Constraint.Factory<Constraint, Object> {
+        @SuppressWarnings("unchecked")
+        @Override
+        public org.spongepowered.configurate.objectmapping.meta.Constraint<Object> make(final Constraint data, final Type type) {
+            try {
+                final Constructor<? extends org.spongepowered.configurate.objectmapping.meta.Constraint<?>> constructor = data.value().getDeclaredConstructor();
+                constructor.trySetAccessible();
+                return (org.spongepowered.configurate.objectmapping.meta.Constraint<Object>) constructor.newInstance();
+            } catch (final ReflectiveOperationException e) {
+                throw new RuntimeException("Could not create constraint", e);
+            }
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java b/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java
new file mode 100644
index 0000000000000000000000000000000000000000..b470332f542c30c42355adb711ff148e8e1dd7a1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/constraint/Constraints.java
@@ -0,0 +1,74 @@
+package io.papermc.paper.configuration.constraint;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.configuration.GlobalConfiguration;
+import io.papermc.paper.configuration.type.DoubleOrDefault;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.spongepowered.configurate.objectmapping.meta.Constraint;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Type;
+import java.util.OptionalDouble;
+
+public final class Constraints {
+    private Constraints() {
+    }
+
+    public static final class Velocity implements Constraint<GlobalConfiguration.Proxies.Velocity> {
+
+        private static final Logger LOGGER = LogUtils.getLogger();
+
+        @Override
+        public void validate(final GlobalConfiguration.Proxies.@Nullable Velocity value) throws SerializationException {
+            if (value != null && value.enabled && value.secret.isEmpty()) {
+                LOGGER.error("Velocity is enabled, but no secret key was specified. A secret key is required. Disabling velocity...");
+                value.enabled = false;
+            }
+        }
+    }
+
+    public static final class Positive implements Constraint<Number> {
+        @Override
+        public void validate(@Nullable Number value) throws SerializationException {
+            if (value != null && value.doubleValue() <= 0) {
+                throw new SerializationException(value + " should be positive");
+            }
+        }
+    }
+
+    public static final class BelowZeroDoubleToDefault implements Constraint<DoubleOrDefault> {
+        @Override
+        public void validate(final @Nullable DoubleOrDefault container) {
+            if (container != null) {
+                final OptionalDouble value = container.value();
+                if (value.isPresent() && value.getAsDouble() < 0) {
+                    container.value(OptionalDouble.empty());
+                }
+            }
+        }
+    }
+
+    @Documented
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public @interface Min {
+        int value();
+
+        final class Factory implements Constraint.Factory<Min, Number> {
+            @Override
+            public Constraint<Number> make(Min data, Type type) {
+                return value -> {
+                    if (value != null && value.intValue() < data.value()) {
+                        throw new SerializationException(value + " is less than the min " + data.value());
+                    }
+                };
+            }
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java b/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..62b43280f59163f7910f79cc901b50d05cdd024e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/legacy/MaxEntityCollisionsInitializer.java
@@ -0,0 +1,29 @@
+package io.papermc.paper.configuration.legacy;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
+import org.spongepowered.configurate.util.NamingSchemes;
+
+public class MaxEntityCollisionsInitializer implements NodeResolver {
+
+    private final String name;
+    private final SpigotWorldConfig spigotConfig;
+
+    public MaxEntityCollisionsInitializer(String name, SpigotWorldConfig spigotConfig) {
+        this.name = name;
+        this.spigotConfig = spigotConfig;
+    }
+
+    @Override
+    public @Nullable ConfigurationNode resolve(ConfigurationNode parent) {
+        final String key = NamingSchemes.LOWER_CASE_DASHED.coerce(this.name);
+        final ConfigurationNode node = parent.node(key);
+        final int old = this.spigotConfig.getInt("max-entity-collisions", -1, false);
+        if (node.virtual() && old > -1) {
+            node.raw(old);
+        }
+        return node;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java b/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java
new file mode 100644
index 0000000000000000000000000000000000000000..611bdbcef3d52e09179aa8b1677ab1e198c70b02
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/legacy/RequiresSpigotInitialization.java
@@ -0,0 +1,51 @@
+package io.papermc.paper.configuration.legacy;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Constructor;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface RequiresSpigotInitialization {
+
+    Class<? extends NodeResolver> value();
+
+    final class Factory implements NodeResolver.Factory {
+
+        private final SpigotWorldConfig spigotWorldConfig;
+        private final Table<Class<? extends NodeResolver>, String, NodeResolver> cache = HashBasedTable.create();
+
+        public Factory(SpigotWorldConfig spigotWorldConfig) {
+            this.spigotWorldConfig = spigotWorldConfig;
+        }
+
+        @Override
+        public @Nullable NodeResolver make(String name, AnnotatedElement element) {
+            if (element.isAnnotationPresent(RequiresSpigotInitialization.class)) {
+                return this.cache.row(element.getAnnotation(RequiresSpigotInitialization.class).value()).computeIfAbsent(name, key -> {
+                    try {
+                        final Constructor<? extends NodeResolver> constructor = element.getAnnotation(RequiresSpigotInitialization.class).value().getDeclaredConstructor(String.class, SpigotWorldConfig.class);
+                        constructor.trySetAccessible();
+                        return constructor.newInstance(key, this.spigotWorldConfig);
+                    } catch (final ReflectiveOperationException e) {
+                        throw new RuntimeException("Could not create constraint", e);
+                    }
+                });
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java b/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..fe5cc1c097f8d8c135e6ead6f458426bb84a8ebe
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/legacy/SpawnLoadedRangeInitializer.java
@@ -0,0 +1,27 @@
+package io.papermc.paper.configuration.legacy;
+
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
+import org.spongepowered.configurate.util.NamingSchemes;
+
+public final class SpawnLoadedRangeInitializer implements NodeResolver {
+
+    private final String name;
+    private final SpigotWorldConfig spigotConfig;
+
+    public SpawnLoadedRangeInitializer(String name, SpigotWorldConfig spigotConfig) {
+        this.name = name;
+        this.spigotConfig = spigotConfig;
+    }
+
+    @Override
+    public ConfigurationNode resolve(ConfigurationNode parent) {
+        final String key = NamingSchemes.LOWER_CASE_DASHED.coerce(this.name);
+        final ConfigurationNode node = parent.node(key);
+        if (node.virtual()) {
+            node.raw(Math.min(spigotConfig.viewDistance, 10));
+        }
+        return node;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/package-info.java b/src/main/java/io/papermc/paper/configuration/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e3bcd7c478096384fcc643d48771ab94318deb3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/package-info.java
@@ -0,0 +1,5 @@
+@DefaultQualifier(NonNull.class)
+package io.papermc.paper.configuration;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
\ No newline at end of file
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c339ef178ebc3b0251095f320e4a7a3656d3521
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/ComponentSerializer.java
@@ -0,0 +1,26 @@
+package io.papermc.paper.configuration.serializer;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.function.Predicate;
+
+public class ComponentSerializer extends ScalarSerializer<Component> {
+
+    public ComponentSerializer() {
+        super(Component.class);
+    }
+
+    @Override
+    public Component deserialize(Type type, Object obj) throws SerializationException {
+        return MiniMessage.miniMessage().deserialize(obj.toString());
+    }
+
+    @Override
+    protected Object serialize(Component component, Predicate<Class<?>> typeSupported) {
+        return MiniMessage.miniMessage().serialize(component);
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..27c0679d376bb31ab52131dfea74b3b580ca92b5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java
@@ -0,0 +1,33 @@
+package io.papermc.paper.configuration.serializer;
+
+import io.papermc.paper.configuration.type.EngineMode;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.function.Predicate;
+
+public final class EngineModeSerializer extends ScalarSerializer<EngineMode> {
+
+    public EngineModeSerializer() {
+        super(EngineMode.class);
+    }
+
+    @Override
+    public EngineMode deserialize(Type type, Object obj) throws SerializationException {
+        if (obj instanceof Integer id) {
+            try {
+                return EngineMode.valueOf(id);
+            } catch (IllegalArgumentException e) {
+                throw new SerializationException(id + " is not a valid id for type " + type + " for this node");
+            }
+        }
+
+        throw new SerializationException(obj + " is not of a valid type " + type + " for this node");
+    }
+
+    @Override
+    protected Object serialize(EngineMode item, Predicate<Class<?>> typeSupported) {
+        return item.getId();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..2afb9268447792e3cdb46172b2050dbce066c59a
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java
@@ -0,0 +1,50 @@
+package io.papermc.paper.configuration.serializer;
+
+import com.mojang.logging.LogUtils;
+import io.leangen.geantyref.TypeToken;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.util.EnumLookup;
+
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+
+import static io.leangen.geantyref.GenericTypeReflector.erase;
+
+/**
+ * Enum serializer that lists options if fails and accepts `-` as `_`.
+ */
+public class EnumValueSerializer extends ScalarSerializer<Enum<?>> {
+
+    private static final Logger LOGGER = LogUtils.getLogger();
+
+    public EnumValueSerializer() {
+        super(new TypeToken<Enum<?>>() {});
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    @Override
+    public @Nullable Enum<?> deserialize(final Type type, final Object obj) throws SerializationException {
+        final String enumConstant = obj.toString();
+        final Class<? extends Enum> typeClass = erase(type).asSubclass(Enum.class);
+        @Nullable Enum<?> ret = EnumLookup.lookupEnum(typeClass, enumConstant);
+        if (ret == null) {
+            ret = EnumLookup.lookupEnum(typeClass, enumConstant.replace("-", "_"));
+        }
+        if (ret == null) {
+            boolean longer = typeClass.getEnumConstants().length > 10;
+            List<String> options = Arrays.stream(typeClass.getEnumConstants()).limit(10L).map(Enum::name).toList();
+            LOGGER.error("Invalid enum constant provided, expected one of [" + String.join(", " ,options) + (longer ? ", ..." : "") + "], but got " + enumConstant);
+        }
+        return ret;
+    }
+
+    @Override
+    public Object serialize(final Enum<?> item, final Predicate<Class<?>> typeSupported) {
+        return item.name();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/FastutilMapSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/FastutilMapSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..f2f362883d1825084c277608c791f82165828ebe
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/FastutilMapSerializer.java
@@ -0,0 +1,69 @@
+package io.papermc.paper.configuration.serializer;
+
+import io.leangen.geantyref.GenericTypeReflector;
+import io.leangen.geantyref.TypeFactory;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Function;
+
+@SuppressWarnings("rawtypes")
+public abstract class FastutilMapSerializer<M extends Map<?, ?>> implements TypeSerializer<M> {
+    private final Function<Map, ? extends M> factory;
+
+    protected FastutilMapSerializer(final Function<Map, ? extends M> factory) {
+        this.factory = factory;
+    }
+
+    @Override
+    public M deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
+        @Nullable final Map map = (Map) node.get(this.createBaseMapType((ParameterizedType) type));
+        return this.factory.apply(map == null ? Collections.emptyMap() : map);
+    }
+
+    @Override
+    public void serialize(final Type type, @Nullable final M obj, final ConfigurationNode node) throws SerializationException {
+        if (obj == null || obj.isEmpty()) {
+            node.raw(null);
+        } else {
+            final Type baseMapType = this.createBaseMapType((ParameterizedType) type);
+            node.set(baseMapType, obj);
+        }
+    }
+
+    protected abstract Type createBaseMapType(final ParameterizedType type);
+
+    public static final class SomethingToPrimitive<M extends Map<?, ?>> extends FastutilMapSerializer<M> {
+        private final Type primitiveType;
+
+        public SomethingToPrimitive(final Function<Map, ? extends M> factory, final Type primitiveType) {
+            super(factory);
+            this.primitiveType = primitiveType;
+        }
+
+        @Override
+        protected Type createBaseMapType(final ParameterizedType type) {
+            return TypeFactory.parameterizedClass(Map.class, type.getActualTypeArguments()[0], GenericTypeReflector.box(this.primitiveType));
+        }
+    }
+
+    public static final class PrimitiveToSomething<M extends Map<?, ?>> extends FastutilMapSerializer<M> {
+        private final Type primitiveType;
+
+        public PrimitiveToSomething(final Function<Map, ? extends M> factory, final Type primitiveType) {
+            super(factory);
+            this.primitiveType = primitiveType;
+        }
+
+        @Override
+        protected Type createBaseMapType(final ParameterizedType type) {
+            return TypeFactory.parameterizedClass(Map.class, GenericTypeReflector.box(this.primitiveType), type.getActualTypeArguments()[0]);
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc065d5cc8975dd189954272116a6bc5bc7f4e28
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/PacketClassSerializer.java
@@ -0,0 +1,86 @@
+package io.papermc.paper.configuration.serializer;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.mojang.logging.LogUtils;
+import io.leangen.geantyref.TypeToken;
+import io.papermc.paper.configuration.serializer.collections.MapSerializer;
+import io.papermc.paper.util.ObfHelper;
+import net.minecraft.network.protocol.Packet;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+@SuppressWarnings("Convert2Diamond")
+public final class PacketClassSerializer extends ScalarSerializer<Class<? extends Packet<?>>> implements MapSerializer.WriteBack {
+
+    private static final Logger LOGGER = LogUtils.getLogger();
+    private static final TypeToken<Class<? extends Packet<?>>> TYPE = new TypeToken<Class<? extends Packet<?>>>() {};
+    private static final List<String> SUBPACKAGES = List.of("game", "handshake", "login", "status");
+    private static final BiMap<String, String> MOJANG_TO_OBF;
+
+    static {
+        final ImmutableBiMap.Builder<String, String> builder = ImmutableBiMap.builder();
+        final @Nullable Map<String, ObfHelper.ClassMapping> classMappingMap = ObfHelper.INSTANCE.mappingsByMojangName();
+        if (classMappingMap != null) {
+            classMappingMap.forEach((mojMap, classMapping) -> {
+                if (mojMap.startsWith("net.minecraft.network.protocol.")) {
+                    builder.put(classMapping.mojangName(), classMapping.obfName());
+                }
+            });
+        }
+        MOJANG_TO_OBF = builder.build();
+    }
+
+    public PacketClassSerializer() {
+        super(TYPE);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Class<? extends Packet<?>> deserialize(final Type type, final Object obj) throws SerializationException {
+        @Nullable Class<?> packetClass = null;
+        for (final String subpackage : SUBPACKAGES) {
+            final String fullClassName = "net.minecraft.network.protocol." + subpackage + "." + obj;
+            try {
+                packetClass = Class.forName(fullClassName);
+                break;
+            } catch (final ClassNotFoundException ex) {
+                final @Nullable String spigotClassName = MOJANG_TO_OBF.get(fullClassName);
+                if (spigotClassName != null) {
+                    try {
+                        packetClass = Class.forName(spigotClassName);
+                    } catch (final ClassNotFoundException ignore) {}
+                }
+            }
+        }
+        if (packetClass == null || !Packet.class.isAssignableFrom(packetClass)) {
+            throw new SerializationException("Could not deserialize a packet from " + obj);
+        }
+        return (Class<? extends Packet<?>>) packetClass;
+    }
+
+    @Override
+    protected @Nullable Object serialize(final Class<? extends Packet<?>> packetClass, final Predicate<Class<?>> typeSupported) {
+        final String name = packetClass.getName();
+        @Nullable String mojName = ObfHelper.INSTANCE.mappingsByMojangName() == null ? name : MOJANG_TO_OBF.inverse().get(name); // if the mappings are null, running on moj-mapped server
+        if (mojName == null && MOJANG_TO_OBF.containsKey(name)) {
+            mojName = name;
+        }
+        if (mojName != null) {
+            int pos = mojName.lastIndexOf('.');
+            if (pos != -1 && pos != mojName.length() - 1) {
+                return mojName.substring(pos + 1);
+            }
+        }
+
+        LOGGER.error("Could not serialize {} into a mojang-mapped packet class name", packetClass);
+        return null;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..add9d16bac9e4570fbdcf8368d7ba03116e97ddf
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/StringRepresentableSerializer.java
@@ -0,0 +1,43 @@
+package io.papermc.paper.configuration.serializer;
+
+import net.minecraft.util.StringRepresentable;
+import net.minecraft.world.entity.MobCategory;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public final class StringRepresentableSerializer extends ScalarSerializer<StringRepresentable> {
+    private static final Map<Type, Function<String, StringRepresentable>> TYPES = Collections.synchronizedMap(Map.ofEntries(
+        Map.entry(MobCategory.class, s -> {
+            for (MobCategory value : MobCategory.values()) {
+                if (value.getSerializedName().equals(s)) {
+                    return value;
+                }
+            }
+            return null;
+        })
+    ));
+
+    public StringRepresentableSerializer() {
+        super(StringRepresentable.class);
+    }
+
+    @Override
+    public StringRepresentable deserialize(Type type, Object obj) throws SerializationException {
+        Function<String, StringRepresentable> function = TYPES.get(type);
+        if (function == null) {
+            throw new SerializationException(type + " isn't registered");
+        }
+        return function.apply(obj.toString());
+    }
+
+    @Override
+    protected Object serialize(StringRepresentable item, Predicate<Class<?>> typeSupported) {
+        return item.getSerializedName();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/TableSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/TableSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..346422c5eb791961061cc73b9b827d63bbd67daf
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/TableSerializer.java
@@ -0,0 +1,89 @@
+package io.papermc.paper.configuration.serializer;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import io.leangen.geantyref.TypeFactory;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.BasicConfigurationNode;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.ConfigurationOptions;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Map;
+import java.util.Objects;
+
+public class TableSerializer implements TypeSerializer<Table<?, ?, ?>> {
+    private static final int ROW_TYPE_ARGUMENT_INDEX = 0;
+    private static final int COLUMN_TYPE_ARGUMENT_INDEX = 1;
+    private static final int VALUE_TYPE_ARGUMENT_INDEX = 2;
+
+    @Override
+    public Table<?, ?, ?> deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
+        final Table<?, ?, ?> table = HashBasedTable.create();
+        if (!node.empty() && node.isMap()) {
+            this.deserialize0(table, (ParameterizedType) type, node);
+        }
+        return table;
+    }
+
+    @SuppressWarnings("unchecked")
+    private <R, C, V> void deserialize0(final Table<R, C, V> table, final ParameterizedType type, final ConfigurationNode node) throws SerializationException {
+        final Type rowType = type.getActualTypeArguments()[ROW_TYPE_ARGUMENT_INDEX];
+        final Type columnType = type.getActualTypeArguments()[COLUMN_TYPE_ARGUMENT_INDEX];
+        final Type valueType = type.getActualTypeArguments()[VALUE_TYPE_ARGUMENT_INDEX];
+
+        final @Nullable TypeSerializer<R> rowKeySerializer = (TypeSerializer<R>) node.options().serializers().get(rowType);
+        if (rowKeySerializer == null) {
+            throw new SerializationException("Could not find serializer for table row type " + rowType);
+        }
+
+        final Type mapType = TypeFactory.parameterizedClass(Map.class, columnType, valueType);
+        final @Nullable TypeSerializer<Map<C, V>> columnValueSerializer = (TypeSerializer<Map<C, V>>) node.options().serializers().get(mapType);
+        if (columnValueSerializer == null) {
+            throw new SerializationException("Could not find serializer for table column-value map " + type);
+        }
+
+        final BasicConfigurationNode rowKeyNode = BasicConfigurationNode.root(node.options());
+
+        for (final Object key : node.childrenMap().keySet()) {
+            final R rowKey = rowKeySerializer.deserialize(rowType, rowKeyNode.set(key));
+            final Map<C, V> map = columnValueSerializer.deserialize(mapType, node.node(rowKeyNode.raw()));
+            map.forEach((column, value) -> table.put(rowKey, column, value));
+        }
+    }
+
+    @Override
+    public void serialize(final Type type, @Nullable final Table<?, ?, ?> table, final ConfigurationNode node) throws SerializationException {
+        if (table != null) {
+            this.serialize0(table, (ParameterizedType) type, node);
+        }
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private <R, C, V> void serialize0(final Table<R, C, V> table, final ParameterizedType type, final ConfigurationNode node) throws SerializationException {
+        final Type rowType = type.getActualTypeArguments()[ROW_TYPE_ARGUMENT_INDEX];
+        final Type columnType = type.getActualTypeArguments()[COLUMN_TYPE_ARGUMENT_INDEX];
+        final Type valueType = type.getActualTypeArguments()[VALUE_TYPE_ARGUMENT_INDEX];
+
+        final @Nullable TypeSerializer rowKeySerializer = node.options().serializers().get(rowType);
+        if (rowKeySerializer == null) {
+            throw new SerializationException("Could not find a serializer for table row type " + rowType);
+        }
+
+        final BasicConfigurationNode rowKeyNode = BasicConfigurationNode.root(node.options());
+        for (final R key : table.rowKeySet()) {
+            rowKeySerializer.serialize(rowType, key, rowKeyNode.set(key));
+            final Object keyObj = Objects.requireNonNull(rowKeyNode.raw());
+            node.node(keyObj).set(TypeFactory.parameterizedClass(Map.class, columnType, valueType), table.row(key));
+        }
+    }
+
+    @Override
+    public @Nullable Table<?, ?, ?> emptyValue(Type specificType, ConfigurationOptions options) {
+        return ImmutableTable.of();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..f44d4cb05eab25d79a8ac09b9da981633380c4fc
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/collections/MapSerializer.java
@@ -0,0 +1,162 @@
+package io.papermc.paper.configuration.serializer.collections;
+
+import com.mojang.logging.LogUtils;
+import io.leangen.geantyref.TypeToken;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.spongepowered.configurate.BasicConfigurationNode;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.ConfigurationOptions;
+import org.spongepowered.configurate.NodePath;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Map serializer that does not throw errors on individual entry serialization failures.
+ */
+public class MapSerializer implements TypeSerializer<Map<?, ?>> {
+
+    public static final TypeToken<Map<?, ?>> TYPE = new TypeToken<Map<?, ?>>() {};
+
+    private static final Logger LOGGER = LogUtils.getLogger();
+
+    private final boolean clearInvalids;
+
+    public MapSerializer(boolean clearInvalids) {
+        this.clearInvalids = clearInvalids;
+    }
+
+    @Override
+    public Map<?, ?> deserialize(Type type, ConfigurationNode node) throws SerializationException {
+        final Map<Object, Object> map = new LinkedHashMap<>();
+        if (node.isMap()) {
+            if (!(type instanceof ParameterizedType parameterizedType)) {
+                throw new SerializationException(type, "Raw types are not supported for collections");
+            }
+            if (parameterizedType.getActualTypeArguments().length != 2) {
+                throw new SerializationException(type, "Map expected two type arguments!");
+            }
+            final Type key = parameterizedType.getActualTypeArguments()[0];
+            final Type value = parameterizedType.getActualTypeArguments()[1];
+            final @Nullable TypeSerializer<?> keySerializer = node.options().serializers().get(key);
+            final @Nullable TypeSerializer<?> valueSerializer = node.options().serializers().get(value);
+            if (keySerializer == null) {
+                throw new SerializationException(type, "No type serializer available for key type " + key);
+            }
+            if (valueSerializer == null) {
+                throw new SerializationException(type, "No type serializer available for value type " + value);
+            }
+
+            final BasicConfigurationNode keyNode = BasicConfigurationNode.root(node.options());
+            final Set<Object> keysToClear = new HashSet<>();
+            for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) {
+                final @Nullable Object deserializedKey = deserialize(key, keySerializer, "key", keyNode.set(ent.getKey()), node.path());
+                final @Nullable Object deserializedValue = deserialize(value, valueSerializer, "value", ent.getValue(), ent.getValue().path());
+                if (deserializedKey == null || deserializedValue == null) {
+                    continue;
+                }
+                if (keySerializer instanceof WriteBack) {
+                    if (serialize(key, keySerializer, deserializedKey, "key", keyNode, node.path()) && !ent.getKey().equals(requireNonNull(keyNode.raw(), "Key must not be null!"))) {
+                        keysToClear.add(ent.getKey());
+                    }
+                }
+                map.put(deserializedKey, deserializedValue);
+            }
+            if (keySerializer instanceof WriteBack) { // supports cleaning keys which deserialize to the same value
+                for (Object keyToClear : keysToClear) {
+                    node.node(keyToClear).raw(null);
+                }
+            }
+        }
+        return map;
+    }
+
+    private @Nullable Object deserialize(Type type, TypeSerializer<?> serializer, String mapPart, ConfigurationNode node, NodePath path) {
+        try {
+            return serializer.deserialize(type, node);
+        } catch (SerializationException ex) {
+            ex.initPath(node::path);
+            LOGGER.error("Could not deserialize {} {} into {} at {}", mapPart, node.raw(), type, path);
+        }
+        return null;
+    }
+
+    @Override
+    public void serialize(Type type, @Nullable Map<?, ?> obj, ConfigurationNode node) throws SerializationException {
+        if (!(type instanceof ParameterizedType parameterizedType)) {
+            throw new SerializationException(type, "Raw types are not supported for collections");
+        }
+        if (parameterizedType.getActualTypeArguments().length != 2) {
+            throw new SerializationException(type, "Map expected two type arguments!");
+        }
+        final Type key = parameterizedType.getActualTypeArguments()[0];
+        final Type value = parameterizedType.getActualTypeArguments()[1];
+        final @Nullable TypeSerializer<?> keySerializer = node.options().serializers().get(key);
+        final @Nullable TypeSerializer<?> valueSerializer = node.options().serializers().get(value);
+
+        if (keySerializer == null) {
+            throw new SerializationException(type, "No type serializer available for key type " + key);
+        }
+
+        if (valueSerializer == null) {
+            throw new SerializationException(type, "No type serializer available for value type " + value);
+        }
+
+        if (obj == null || obj.isEmpty()) {
+            node.set(Collections.emptyMap());
+        } else {
+            final Set<Object> unvisitedKeys;
+            if (node.empty()) {
+                node.raw(Collections.emptyMap());
+                unvisitedKeys = Collections.emptySet();
+            } else {
+                unvisitedKeys = new HashSet<>(node.childrenMap().keySet());
+            }
+            final BasicConfigurationNode keyNode = BasicConfigurationNode.root(node.options());
+            for (Map.Entry<?, ?> ent : obj.entrySet()) {
+                if (!serialize(key, keySerializer, ent.getKey(), "key", keyNode, node.path())) {
+                    continue;
+                }
+                final Object keyObj = requireNonNull(keyNode.raw(), "Key must not be null!");
+                final ConfigurationNode child = node.node(keyObj);
+                serialize(value, valueSerializer, ent.getValue(), "value", child, child.path());
+                unvisitedKeys.remove(keyObj);
+            }
+            if (this.clearInvalids) {
+                for (Object unusedChild : unvisitedKeys) {
+                    node.removeChild(unusedChild);
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private boolean serialize(Type type, TypeSerializer serializer, Object object, String mapPart, ConfigurationNode node, NodePath path) {
+        try {
+            serializer.serialize(type, object, node);
+            return true;
+        } catch (SerializationException ex) {
+            ex.initPath(node::path);
+            LOGGER.error("Could not serialize {} {} from {} at {}", mapPart, object, type, path);
+        }
+        return false;
+    }
+
+    @Override
+    public @Nullable Map<?, ?> emptyValue(Type specificType, ConfigurationOptions options) {
+        return new LinkedHashMap<>();
+    }
+
+    public interface WriteBack { // marker interface
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e4e0f1788cf67312cb52bd572784c2f27db71b6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryEntrySerializer.java
@@ -0,0 +1,62 @@
+package io.papermc.paper.configuration.serializer.registry;
+
+import io.leangen.geantyref.TypeToken;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.function.Predicate;
+
+abstract class RegistryEntrySerializer<T, R> extends ScalarSerializer<T> {
+
+    private final ResourceKey<? extends Registry<R>> registryKey;
+    private final boolean omitMinecraftNamespace;
+
+    protected RegistryEntrySerializer(TypeToken<T> type, ResourceKey<? extends Registry<R>> registryKey, boolean omitMinecraftNamespace) {
+        super(type);
+        this.registryKey = registryKey;
+        this.omitMinecraftNamespace = omitMinecraftNamespace;
+    }
+
+    protected RegistryEntrySerializer(Class<T> type, ResourceKey<? extends Registry<R>> registryKey, boolean omitMinecraftNamespace) {
+        super(type);
+        this.registryKey = registryKey;
+        this.omitMinecraftNamespace = omitMinecraftNamespace;
+    }
+
+    protected final Registry<R> registry() {
+        return MinecraftServer.getServer().registryAccess().registryOrThrow(this.registryKey);
+    }
+
+    protected abstract T convertFromResourceKey(ResourceKey<R> key) throws SerializationException;
+
+    @Override
+    public final T deserialize(Type type, Object obj) throws SerializationException {
+        return this.convertFromResourceKey(this.deserializeKey(obj));
+    }
+
+    protected abstract ResourceKey<R> convertToResourceKey(T value);
+
+    @Override
+    protected final Object serialize(T item, Predicate<Class<?>> typeSupported) {
+        final ResourceKey<R> key = this.convertToResourceKey(item);
+        if (this.omitMinecraftNamespace && key.location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) {
+            return key.location().getPath();
+        } else {
+            return key.location().toString();
+        }
+    }
+
+    private ResourceKey<R> deserializeKey(final Object input) throws SerializationException {
+        final @Nullable ResourceLocation key = ResourceLocation.tryParse(input.toString());
+        if (key == null) {
+            throw new SerializationException("Could not create a key from " + input);
+        }
+        return ResourceKey.create(this.registryKey, key);
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..c03c1f277ff8167e8b3e4bfa0f4dfc86834f82f3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryHolderSerializer.java
@@ -0,0 +1,34 @@
+package io.papermc.paper.configuration.serializer.registry;
+
+import com.google.common.base.Preconditions;
+import io.leangen.geantyref.TypeFactory;
+import io.leangen.geantyref.TypeToken;
+import net.minecraft.core.Holder;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceKey;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.util.function.Function;
+
+public final class RegistryHolderSerializer<T> extends RegistryEntrySerializer<Holder<T>, T> {
+
+    @SuppressWarnings("unchecked")
+    public RegistryHolderSerializer(TypeToken<T> typeToken, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
+        super((TypeToken<Holder<T>>) TypeToken.get(TypeFactory.parameterizedClass(Holder.class, typeToken.getType())), registryKey, omitMinecraftNamespace);
+    }
+
+    public RegistryHolderSerializer(Class<T> type, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
+        this(TypeToken.get(type), registryKey, omitMinecraftNamespace);
+        Preconditions.checkArgument(type.getTypeParameters().length == 0, "%s must have 0 type parameters", type);
+    }
+
+    @Override
+    protected Holder<T> convertFromResourceKey(ResourceKey<T> key) throws SerializationException {
+        return this.registry().getHolder(key).orElseThrow(() -> new SerializationException("Missing holder in " + this.registry().key() + " with key " + key));
+    }
+
+    @Override
+    protected ResourceKey<T> convertToResourceKey(Holder<T> value) {
+        return value.unwrap().map(Function.identity(), r -> this.registry().getResourceKey(r).orElseThrow());
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..10d3dd361cd26dc849ebd53c1235aa8e4f7af04d
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/serializer/registry/RegistryValueSerializer.java
@@ -0,0 +1,34 @@
+package io.papermc.paper.configuration.serializer.registry;
+
+import io.leangen.geantyref.TypeToken;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceKey;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+/**
+ * Use {@link RegistryHolderSerializer} for datapack-configurable things.
+ */
+public final class RegistryValueSerializer<T> extends RegistryEntrySerializer<T, T> {
+
+    public RegistryValueSerializer(TypeToken<T> type, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
+        super(type, registryKey, omitMinecraftNamespace);
+    }
+
+    public RegistryValueSerializer(Class<T> type, ResourceKey<? extends Registry<T>> registryKey, boolean omitMinecraftNamespace) {
+        super(type, registryKey, omitMinecraftNamespace);
+    }
+
+    @Override
+    protected T convertFromResourceKey(ResourceKey<T> key) throws SerializationException {
+        final T value = this.registry().get(key);
+        if (value == null) {
+            throw new SerializationException("Missing value in " + this.registry() + " with key " + key.location());
+        }
+        return value;
+    }
+
+    @Override
+    protected ResourceKey<T> convertToResourceKey(T value) {
+        return this.registry().getResourceKey(value).orElseThrow();
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java b/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java
new file mode 100644
index 0000000000000000000000000000000000000000..0300fb1e09d41465e4a50bfdc987b9571289d399
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/transformation/Transformations.java
@@ -0,0 +1,35 @@
+package io.papermc.paper.configuration.transformation;
+
+import io.papermc.paper.configuration.Configurations;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.NodePath;
+import org.spongepowered.configurate.transformation.ConfigurationTransformation;
+
+import static org.spongepowered.configurate.NodePath.path;
+
+public final class Transformations {
+    private Transformations() {
+    }
+
+    public static void moveFromRoot(final ConfigurationTransformation.Builder builder, final String key, final String... parents) {
+        moveFromRootAndRename(builder, key, key, parents);
+    }
+
+    public static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) {
+        moveFromRootAndRename(builder, path(oldKey), newKey, parents);
+    }
+
+    public static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final NodePath oldKey, final String newKey, final String... parents) {
+        builder.addAction(oldKey, (path, value) -> {
+            final Object[] newPath = new Object[parents.length + 1];
+            newPath[parents.length] = newKey;
+            System.arraycopy(parents, 0, newPath, 0, parents.length);
+            return newPath;
+        });
+    }
+
+    @FunctionalInterface
+    public interface DefaultsAware {
+        void apply(final ConfigurationTransformation.Builder builder, final Configurations.ContextMap contextMap, final ConfigurationNode defaultsNode);
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java b/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..d21335930652ffced22f6fd19ab1a4f9ad599db8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java
@@ -0,0 +1,222 @@
+package io.papermc.paper.configuration.transformation.global;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.configuration.Configuration;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket;
+import org.bukkit.ChatColor;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.transformation.ConfigurationTransformation;
+import org.spongepowered.configurate.transformation.TransformAction;
+
+import java.util.function.Predicate;
+
+import static org.spongepowered.configurate.NodePath.path;
+
+public final class LegacyPaperConfig {
+    private static final Logger LOGGER = LogUtils.getLogger();
+
+    private LegacyPaperConfig() {
+    }
+
+    public static ConfigurationTransformation transformation(final YamlConfiguration spigotConfiguration) {
+        return ConfigurationTransformation.chain(versioned(), notVersioned(spigotConfiguration));
+    }
+
+    // Represents version transforms lifted directly from the old PaperConfig class
+    // must be run BEFORE the "settings" flatten
+    private static ConfigurationTransformation.Versioned versioned() {
+        return ConfigurationTransformation.versionedBuilder()
+            .versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD)
+            .addVersion(11, ConfigurationTransformation.builder().addAction(path("settings", "play-in-use-item-spam-threshold"), TransformAction.rename("incoming-packet-spam-threshold")).build())
+            .addVersion(14, ConfigurationTransformation.builder().addAction(path("settings", "spam-limiter", "tab-spam-increment"), (path, value) -> {
+                if (value.getInt() == 10) {
+                    value.set(2);
+                }
+                return null;
+            }).build())
+            .addVersion(15, ConfigurationTransformation.builder().addAction(path("settings"), (path, value) -> {
+                value.node("async-chunks", "threads").set(-1);
+                return null;
+            }).build())
+            .addVersion(21, ConfigurationTransformation.builder().addAction(path("use-display-name-in-quit-message"), (path, value) -> new Object[]{"settings", "use-display-name-in-quit-message"}).build())
+            .addVersion(23, ConfigurationTransformation.builder().addAction(path("settings", "chunk-loading", "global-max-chunk-load-rate"), (path, value) -> {
+                if (value.getDouble() == 300.0) {
+                    value.set(-1.0);
+                }
+                return null;
+            }).build())
+            .addVersion(25, ConfigurationTransformation.builder().addAction(path("settings", "chunk-loading", "player-max-concurrent-loads"), (path, value) -> {
+                if (value.getDouble() == 4.0) {
+                    value.set(20.0);
+                }
+                return null;
+            }).build())
+            .build();
+    }
+
+    // other non-versioned transforms found in PaperConfig
+    // must be run BEFORE the "settings" flatten
+    private static ConfigurationTransformation notVersioned(final YamlConfiguration spigotConfiguration) {
+        return ConfigurationTransformation.builder()
+            .addAction(path("settings"), (path, value) -> {
+                final ConfigurationNode node = value.node("async-chunks");
+                if (node.hasChild("load-threads")) {
+                    if (!node.hasChild("threads")) {
+                        node.node("threads").set(node.node("load-threads").getInt());
+                    }
+                    node.removeChild("load-threads");
+                }
+                node.removeChild("generation");
+                node.removeChild("enabled");
+                node.removeChild("thread-per-world-generation");
+                return null;
+            })
+            .addAction(path("allow-perm-block-break-exploits"), (path, value) -> new Object[]{"settings", "unsupported-settings", "allow-permanent-block-break-exploits"})
+            .addAction(path("settings", "unsupported-settings", "allow-tnt-duplication"), TransformAction.rename("allow-piston-duplication"))
+            .addAction(path("settings", "save-player-data"), (path, value) -> {
+                final @Nullable Object val = value.raw();
+                if (val instanceof Boolean bool) {
+                    spigotConfiguration.set("players.disable-saving", !bool);
+                }
+                value.raw(null);
+                return null;
+            })
+            .addAction(path("settings", "log-named-entity-deaths"), (path, value) -> {
+                final @Nullable Object val = value.raw();
+                if (val instanceof Boolean bool && !bool) {
+                    spigotConfiguration.set("settings.log-named-deaths", false);
+                }
+                value.raw(null);
+                return null;
+            })
+            .build();
+    }
+
+    // transforms to new format with configurate
+    // must be run AFTER the "settings" flatten
+    public static ConfigurationTransformation toNewFormat() {
+        return ConfigurationTransformation.chain(
+            ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD).addVersion(Configuration.FINAL_LEGACY_VERSION + 1, newFormatTransformation()).build(),
+            ConfigurationTransformation.builder().addAction(path(Configuration.LEGACY_CONFIG_VERSION_FIELD), TransformAction.rename(Configuration.VERSION_FIELD)).build() // rename to _version to place at the top
+        );
+    }
+
+    private static ConfigurationTransformation newFormatTransformation() {
+        final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder()
+            .addAction(path("verbose"), TransformAction.remove()) // not needed
+            .addAction(path("unsupported-settings", "allow-headless-pistons-readme"), TransformAction.remove())
+            .addAction(path("unsupported-settings", "allow-permanent-block-break-exploits-readme"), TransformAction.remove())
+            .addAction(path("unsupported-settings", "allow-piston-duplication-readme"), TransformAction.remove())
+            .addAction(path("packet-limiter", "limits", "all"), (path, value) -> new Object[]{"packet-limiter", "all-packets"})
+            .addAction(path("packet-limiter", "limits"), (path, value) -> new Object[]{"packet-limiter", "overrides"})
+            .addAction(path("packet-limiter", "overrides", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> {
+                final @Nullable Object keyValue = value.key();
+                if (keyValue != null && keyValue.toString().equals("PacketPlayInAutoRecipe")) { // add special cast to handle the default for moj-mapped servers that upgrade the config
+                    return path.with(path.size() - 1, ServerboundPlaceRecipePacket.class.getSimpleName()).array();
+                }
+                return null;
+            }).addAction(path("loggers"), TransformAction.rename("logging"));
+
+        moveFromRootAndRename(builder, "incoming-packet-spam-threshold", "incoming-packet-threshold", "spam-limiter");
+
+        moveFromRoot(builder, "save-empty-scoreboard-teams", "scoreboards");
+        moveFromRoot(builder, "track-plugin-scoreboards", "scoreboards");
+
+        moveFromRoot(builder, "suggest-player-names-when-null-tab-completions", "commands");
+        moveFromRoot(builder, "time-command-affects-all-worlds", "commands");
+        moveFromRoot(builder, "fix-target-selector-tag-completion", "commands");
+
+        moveFromRoot(builder, "log-player-ip-addresses", "loggers");
+
+        moveFromRoot(builder, "use-display-name-in-quit-message", "messages");
+
+        moveFromRootAndRename(builder, "console-has-all-permissions", "has-all-permissions", "console");
+
+        moveFromRootAndRename(builder, "bungee-online-mode", "online-mode", "proxies", "bungee-cord");
+        moveFromRootAndRename(builder, "velocity-support", "velocity", "proxies");
+
+        moveFromRoot(builder, "book-size", "item-validation");
+        moveFromRoot(builder, "resolve-selectors-in-books", "item-validation");
+
+        moveFromRoot(builder, "enable-player-collisions", "collisions");
+        moveFromRoot(builder, "send-full-pos-for-hard-colliding-entities", "collisions");
+
+        moveFromRootAndRename(builder, "player-auto-save-rate", "rate", "player-auto-save");
+        moveFromRootAndRename(builder, "max-player-auto-save-per-tick", "max-per-tick", "player-auto-save");
+
+        moveFromRootToMisc(builder, "max-joins-per-tick");
+        moveFromRootToMisc(builder, "fix-entity-position-desync");
+        moveFromRootToMisc(builder, "load-permissions-yml-before-plugins");
+        moveFromRootToMisc(builder, "region-file-cache-size");
+        moveFromRootToMisc(builder, "use-alternative-luck-formula");
+        moveFromRootToMisc(builder, "lag-compensate-block-breaking");
+        moveFromRootToMisc(builder, "use-dimension-type-for-custom-spawners");
+
+        moveFromRoot(builder, "proxy-protocol", "proxies");
+
+        miniMessageWithTranslatable(builder, String::isBlank, "multiplayer.disconnect.authservers_down", "messages", "kick", "authentication-servers-down");
+        miniMessageWithTranslatable(builder, Predicate.isEqual("Flying is not enabled on this server"), "multiplayer.disconnect.flying", "messages", "kick", "flying-player");
+        miniMessageWithTranslatable(builder, Predicate.isEqual("Flying is not enabled on this server"), "multiplayer.disconnect.flying", "messages", "kick", "flying-vehicle");
+        miniMessage(builder, "messages", "kick", "connection-throttle");
+        miniMessage(builder, "messages", "no-permission");
+        miniMessageWithTranslatable(builder, Predicate.isEqual("&cSent too many packets"), Component.translatable("disconnect.exceeded_packet_rate", NamedTextColor.RED), "packet-limiter", "kick-message");
+
+        return builder.build();
+    }
+
+    private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final String i18nKey, final String... strPath) {
+        miniMessageWithTranslatable(builder, englishCheck, Component.translatable(i18nKey), strPath);
+    }
+    private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final Component component, final String... strPath) {
+        builder.addAction(path((Object[]) strPath), (path, value) -> {
+            final @Nullable Object val = value.raw();
+            if (val != null) {
+                final String strVal = val.toString();
+                if (!englishCheck.test(strVal)) {
+                    value.set(miniMessage(strVal));
+                    return null;
+                }
+            }
+            value.set(MiniMessage.miniMessage().serialize(component));
+            return null;
+        });
+    }
+
+    private static void miniMessage(final ConfigurationTransformation.Builder builder, final String... strPath) {
+        builder.addAction(path((Object[]) strPath), (path, value) -> {
+            final @Nullable Object val = value.raw();
+            if (val != null) {
+                value.set(miniMessage(val.toString()));
+            }
+            return null;
+        });
+    }
+
+    private static String miniMessage(final String input) {
+        return MiniMessage.miniMessage().serialize(LegacyComponentSerializer.legacySection().deserialize(ChatColor.translateAlternateColorCodes('&', input)));
+    }
+
+    private static void moveFromRootToMisc(final ConfigurationTransformation.Builder builder, final String key) {
+        moveFromRoot(builder, key, "misc");
+    }
+
+    private static void moveFromRoot(final ConfigurationTransformation.Builder builder, final String key, final String... parents) {
+        moveFromRootAndRename(builder, key, key, parents);
+    }
+
+    private static void moveFromRootAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) {
+        builder.addAction(path(oldKey), (path, value) -> {
+            final Object[] newPath = new Object[parents.length + 1];
+            newPath[parents.length] = newKey;
+            System.arraycopy(parents, 0, newPath, 0, parents.length);
+            return newPath;
+        });
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java b/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java
new file mode 100644
index 0000000000000000000000000000000000000000..75f612b04f872d0d014fdc40b07c15116857587b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/transformation/world/FeatureSeedsGeneration.java
@@ -0,0 +1,71 @@
+package io.papermc.paper.configuration.transformation.world;
+
+import com.mojang.logging.LogUtils;
+import io.leangen.geantyref.TypeToken;
+import io.papermc.paper.configuration.Configurations;
+import it.unimi.dsi.fastutil.objects.Reference2LongMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
+import net.minecraft.core.Holder;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.spongepowered.configurate.ConfigurateException;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.NodePath;
+import org.spongepowered.configurate.transformation.ConfigurationTransformation;
+import org.spongepowered.configurate.transformation.TransformAction;
+
+import java.security.SecureRandom;
+import java.util.Objects;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.spongepowered.configurate.NodePath.path;
+
+public class FeatureSeedsGeneration implements TransformAction {
+
+    public static final String FEATURE_SEEDS_KEY = "feature-seeds";
+    public static final String GENERATE_KEY = "generate-random-seeds-for-all";
+    public static final String FEATURES_KEY = "features";
+
+    private static final Logger LOGGER = LogUtils.getLogger();
+
+    private final ResourceLocation worldKey;
+
+    private FeatureSeedsGeneration(ResourceLocation worldKey) {
+        this.worldKey = worldKey;
+    }
+
+    @Override
+    public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
+        ConfigurationNode featureNode = value.node(FEATURE_SEEDS_KEY, FEATURES_KEY);
+        final Reference2LongMap<Holder<ConfiguredFeature<?, ?>>> features = Objects.requireNonNullElseGet(featureNode.get(new TypeToken<Reference2LongMap<Holder<ConfiguredFeature<?, ?>>>>() {}), Reference2LongOpenHashMap::new);
+        final Random random = new SecureRandom();
+        AtomicInteger counter = new AtomicInteger(0);
+        MinecraftServer.getServer().registryAccess().registryOrThrow(Registry.CONFIGURED_FEATURE_REGISTRY).holders().forEach(holder -> {
+            if (features.containsKey(holder)) {
+                return;
+            }
+
+            final long seed = random.nextLong();
+            features.put(holder, seed);
+            counter.incrementAndGet();
+        });
+        if (counter.get() > 0) {
+            LOGGER.info("Generated {} random feature seeds for {}", counter.get(), this.worldKey);
+            featureNode.raw(null);
+            featureNode.set(new TypeToken<Reference2LongMap<Holder<ConfiguredFeature<?, ?>>>>() {}, features);
+        }
+        return null;
+    }
+
+
+    public static void apply(final ConfigurationTransformation.Builder builder, final Configurations.ContextMap contextMap, final ConfigurationNode defaultsNode) {
+        if (!contextMap.isDefaultWorldContext() && defaultsNode.node(FEATURE_SEEDS_KEY, GENERATE_KEY).getBoolean(false)) {
+            builder.addAction(path(), new FeatureSeedsGeneration(contextMap.require(Configurations.WORLD_KEY)));
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java b/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..6af307481a6752529d87869760945cb140d05bed
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/transformation/world/LegacyPaperWorldConfig.java
@@ -0,0 +1,321 @@
+package io.papermc.paper.configuration.transformation.world;
+
+import io.papermc.paper.configuration.Configuration;
+import io.papermc.paper.configuration.WorldConfiguration;
+import net.minecraft.core.Holder;
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.MobCategory;
+import net.minecraft.world.item.Item;
+import org.bukkit.Material;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.transformation.ConfigurationTransformation;
+import org.spongepowered.configurate.transformation.TransformAction;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+import static io.papermc.paper.configuration.transformation.Transformations.moveFromRoot;
+import static io.papermc.paper.configuration.transformation.Transformations.moveFromRootAndRename;
+import static org.spongepowered.configurate.NodePath.path;
+
+public final class LegacyPaperWorldConfig {
+
+    private LegacyPaperWorldConfig() {
+    }
+
+    public static ConfigurationTransformation transformation() {
+        return ConfigurationTransformation.chain(versioned(), notVersioned());
+    }
+
+    private static ConfigurationTransformation.Versioned versioned() {
+        return ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD)
+            .addVersion(13, ConfigurationTransformation.builder().addAction(path("enable-old-tnt-cannon-behaviors"), TransformAction.rename("prevent-tnt-from-moving-in-water")).build())
+            .addVersion(16, ConfigurationTransformation.builder().addAction(path("use-chunk-inhabited-timer"), (path, value) -> {
+                if (!value.getBoolean(true)) {
+                    value.raw(0);
+                } else {
+                    value.raw(-1);
+                }
+                final Object[] newPath = path.array();
+                newPath[newPath.length - 1] = "fixed-chunk-inhabited-time";
+                return newPath;
+            }).build())
+            .addVersion(18, ConfigurationTransformation.builder().addAction(path("nether-ceiling-void-damage"), (path, value) -> {
+                if (value.getBoolean(false)) {
+                    value.raw(128);
+                } else {
+                    value.raw(0);
+                }
+                final Object[] newPath = path.array();
+                newPath[newPath.length - 1] = "nether-ceiling-void-damage-height";
+                return newPath;
+            }).build())
+            .addVersion(19, ConfigurationTransformation.builder()
+                .addAction(path("anti-xray", "hidden-blocks"), (path, value) -> {
+                    @Nullable final List<String> hiddenBlocks = value.getList(String.class);
+                    if (hiddenBlocks != null) {
+                        hiddenBlocks.remove("lit_redstone_ore");
+                    }
+                    return null;
+                })
+                .addAction(path("anti-xray", "replacement-blocks"), (path, value) -> {
+                    @Nullable final List<String> replacementBlocks = value.getList(String.class);
+                    if (replacementBlocks != null) {
+                        final int index = replacementBlocks.indexOf("planks");
+                        if (index != -1) {
+                            replacementBlocks.set(index, "oak_planks");
+                        }
+                    }
+                    value.raw(replacementBlocks);
+                    return null;
+                }).build())
+            .addVersion(20, ConfigurationTransformation.builder().addAction(path("baby-zombie-movement-speed"), TransformAction.rename("baby-zombie-movement-modifier")).build())
+            .addVersion(22, ConfigurationTransformation.builder().addAction(path("per-player-mob-spawns"), (path, value) -> {
+                value.raw(true);
+                return null;
+            }).build())
+            .addVersion(24,
+                ConfigurationTransformation.builder()
+                    .addAction(path("spawn-limits", "monsters"), TransformAction.rename("monster"))
+                    .addAction(path("spawn-limits", "animals"), TransformAction.rename("creature"))
+                    .addAction(path("spawn-limits", "water-animals"), TransformAction.rename("water_creature"))
+                    .addAction(path("spawn-limits", "water-ambient"), TransformAction.rename("water_ambient"))
+                    .build(),
+                ConfigurationTransformation.builder().addAction(path("despawn-ranges"), (path, value) -> {
+                    final int softDistance = value.node("soft").getInt(32);
+                    final int hardDistance = value.node("hard").getInt(128);
+                    value.node("soft").raw(null);
+                    value.node("hard").raw(null);
+                    for (final MobCategory category : MobCategory.values()) {
+                        if (softDistance != 32) {
+                            value.node(category.getName(), "soft").raw(softDistance);
+                        }
+                        if (hardDistance != 128) {
+                            value.node(category.getName(), "hard").raw(hardDistance);
+                        }
+                    }
+                    return null;
+                }).build()
+            )
+            .addVersion(26, ConfigurationTransformation.builder().addAction(path("alt-item-despawn-rate", "items", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> {
+                String itemName = path.get(path.size() - 1).toString();
+                final Optional<Holder<Item>> item = Registry.ITEM.getHolder(ResourceKey.create(Registry.ITEM_REGISTRY, new ResourceLocation(itemName.toLowerCase(Locale.ENGLISH))));
+                if (item.isEmpty()) {
+                    itemName = Material.valueOf(itemName).getKey().getKey();
+                }
+                final Object[] newPath = path.array();
+                newPath[newPath.length - 1] = itemName;
+                return newPath;
+            }).build())
+            .addVersion(27, ConfigurationTransformation.builder().addAction(path("use-faster-eigencraft-redstone"), (path, value) -> {
+                final WorldConfiguration.Misc.RedstoneImplementation redstoneImplementation = value.getBoolean(false) ? WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT : WorldConfiguration.Misc.RedstoneImplementation.VANILLA;
+                value.set(redstoneImplementation);
+                final Object[] newPath = path.array();
+                newPath[newPath.length - 1] = "redstone-implementation";
+                return newPath;
+            }).build())
+            .build();
+    }
+
+    // other transformations found in PaperWorldConfig that aren't versioned
+    private static ConfigurationTransformation notVersioned() {
+        return ConfigurationTransformation.builder()
+            .addAction(path("treasure-maps-return-already-discovered"), (path, value) -> {
+                boolean prevValue = value.getBoolean(false);
+                value.node("villager-trade").set(prevValue);
+                value.node("loot-tables").set(prevValue);
+                return path.with(path.size() - 1, "treasure-maps-find-already-discovered").array();
+            })
+            .addAction(path("alt-item-despawn-rate", "items"), (path, value) -> {
+                if (value.isMap()) {
+                    Map<String, Integer> rebuild = new HashMap<>();
+                    value.childrenMap().forEach((key, node) -> {
+                        String itemName = key.toString();
+                        final Optional<Holder<Item>> itemHolder = Registry.ITEM.getHolder(ResourceKey.create(Registry.ITEM_REGISTRY, new ResourceLocation(itemName.toLowerCase(Locale.ENGLISH))));
+                        final @Nullable String item;
+                        if (itemHolder.isEmpty()) {
+                            final @Nullable Material bukkitMat = Material.matchMaterial(itemName);
+                            item = bukkitMat != null ? bukkitMat.getKey().getKey() : null;
+                        } else {
+                            item = itemHolder.get().unwrapKey().orElseThrow().location().getPath();
+                        }
+                        if (item != null) {
+                            rebuild.put(item, node.getInt());
+                        }
+                    });
+                    value.set(rebuild);
+                }
+                return null;
+            })
+            .build();
+    }
+
+    public static ConfigurationTransformation toNewFormat() {
+        return ConfigurationTransformation.chain(ConfigurationTransformation.versionedBuilder().versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD).addVersion(Configuration.FINAL_LEGACY_VERSION + 1, newFormatTransformation()).build(), ConfigurationTransformation.builder().addAction(path(Configuration.LEGACY_CONFIG_VERSION_FIELD), TransformAction.rename(Configuration.VERSION_FIELD)).build());
+    }
+
+    private static ConfigurationTransformation newFormatTransformation() {
+        final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder()
+            .addAction(path("verbose"), TransformAction.remove()); // not needed
+
+        moveFromRoot(builder, "anti-xray", "anticheat");
+
+        moveFromRootAndRename(builder, "armor-stands-do-collision-entity-lookups", "do-collision-entity-lookups", "entities", "armor-stands");
+        moveFromRootAndRename(builder, "armor-stands-tick", "tick", "entities", "armor-stands");
+
+        moveFromRoot(builder, "auto-save-interval", "chunks");
+        moveFromRoot(builder, "delay-chunk-unloads-by", "chunks");
+        moveFromRoot(builder, "entity-per-chunk-save-limit", "chunks");
+        moveFromRoot(builder, "fixed-chunk-inhabited-time", "chunks");
+        moveFromRoot(builder, "max-auto-save-chunks-per-tick", "chunks");
+        moveFromRoot(builder, "prevent-moving-into-unloaded-chunks", "chunks");
+
+        moveFromRoot(builder, "entities-target-with-follow-range", "entities");
+        moveFromRoot(builder, "mob-effects", "entities");
+
+        moveFromRoot(builder, "filter-nbt-data-from-spawn-eggs-and-related", "entities", "spawning");
+        moveFromGameMechanics(builder, "disable-mob-spawner-spawn-egg-transformation", "entities", "spawning");
+        moveFromRoot(builder, "per-player-mob-spawns", "entities", "spawning");
+        moveFromGameMechanics(builder, "scan-for-legacy-ender-dragon", "entities", "spawning");
+        moveFromRoot(builder, "spawn-limits", "entities", "spawning");
+        moveFromRoot(builder, "despawn-ranges", "entities", "spawning");
+        moveFromRoot(builder, "wateranimal-spawn-height", "entities", "spawning");
+        builder.addAction(path("slime-spawn-height", "swamp-biome"), TransformAction.rename("surface-biome"));
+        moveFromRoot(builder, "slime-spawn-height", "entities", "spawning");
+        moveFromRoot(builder, "wandering-trader", "entities", "spawning");
+        moveFromRoot(builder, "all-chunks-are-slime-chunks", "entities", "spawning");
+        moveFromRoot(builder, "skeleton-horse-thunder-spawn-chance", "entities", "spawning");
+        moveFromRoot(builder, "iron-golems-can-spawn-in-air", "entities", "spawning");
+        moveFromRoot(builder, "alt-item-despawn-rate", "entities", "spawning");
+        moveFromRoot(builder, "count-all-mobs-for-spawning", "entities", "spawning");
+        moveFromRoot(builder, "creative-arrow-despawn-rate", "entities", "spawning");
+        moveFromRoot(builder, "non-player-arrow-despawn-rate", "entities", "spawning");
+        moveFromRoot(builder, "monster-spawn-max-light-level", "entities", "spawning");
+
+
+        moveFromRootAndRename(builder, "duplicate-uuid-saferegen-delete-range", "safe-regen-delete-range", "entities", "spawning", "duplicate-uuid");
+
+        moveFromRoot(builder, "baby-zombie-movement-modifier", "entities", "behavior");
+        moveFromRoot(builder, "disable-creeper-lingering-effect", "entities", "behavior");
+        moveFromRoot(builder, "door-breaking-difficulty", "entities", "behavior");
+        moveFromGameMechanics(builder, "disable-chest-cat-detection", "entities", "behavior");
+        moveFromGameMechanics(builder, "disable-player-crits", "entities", "behavior");
+        moveFromRoot(builder, "experience-merge-max-value", "entities", "behavior");
+        moveFromRoot(builder, "mobs-can-always-pick-up-loot", "entities", "behavior");
+        moveFromGameMechanics(builder, "nerf-pigmen-from-nether-portals", "entities", "behavior");
+        moveFromRoot(builder, "parrots-are-unaffected-by-player-movement", "entities", "behavior");
+        moveFromRoot(builder, "phantoms-do-not-spawn-on-creative-players", "entities", "behavior");
+        moveFromRoot(builder, "phantoms-only-attack-insomniacs", "entities", "behavior");
+        moveFromRoot(builder, "piglins-guard-chests", "entities", "behavior");
+        moveFromRoot(builder, "spawner-nerfed-mobs-should-jump", "entities", "behavior");
+        moveFromRoot(builder, "zombie-villager-infection-chance", "entities", "behavior");
+        moveFromRoot(builder, "zombies-target-turtle-eggs", "entities", "behavior");
+        moveFromRoot(builder, "ender-dragons-death-always-places-dragon-egg", "entities", "behavior");
+        moveFromGameMechanicsAndRename(builder, "disable-pillager-patrols", "disable", "game-mechanics", "pillager-patrols");
+        moveFromGameMechanics(builder, "pillager-patrols", "entities", "behavior");
+        moveFromRoot(builder, "should-remove-dragon", "entities", "behavior");
+
+        moveFromRootAndRename(builder, "map-item-frame-cursor-limit", "item-frame-cursor-limit", "maps");
+        moveFromRootAndRename(builder, "map-item-frame-cursor-update-interval", "item-frame-cursor-update-interval", "maps");
+
+        moveFromRootAndRename(builder, "mob-spawner-tick-rate", "mob-spawner", "tick-rates");
+        moveFromRootAndRename(builder, "container-update-tick-rate", "container-update", "tick-rates");
+        moveFromRootAndRename(builder, "grass-spread-tick-rate", "grass-spread", "tick-rates");
+
+        moveFromRoot(builder, "allow-non-player-entities-on-scoreboards", "scoreboards");
+        moveFromRoot(builder, "use-vanilla-world-scoreboard-name-coloring", "scoreboards");
+
+        moveFromRoot(builder, "disable-thunder", "environment");
+        moveFromRoot(builder, "disable-ice-and-snow", "environment");
+        moveFromRoot(builder, "optimize-explosions", "environment");
+        moveFromRoot(builder, "disable-explosion-knockback", "environment");
+        moveFromRoot(builder, "frosted-ice", "environment");
+        moveFromRoot(builder, "disable-teleportation-suffocation-check", "environment");
+        moveFromRoot(builder, "portal-create-radius", "environment");
+        moveFromRoot(builder, "portal-search-radius", "environment");
+        moveFromRoot(builder, "portal-search-vanilla-dimension-scaling", "environment");
+        moveFromRootAndRename(builder, "enable-treasure-maps", "enabled", "environment", "treasure-maps");
+        moveFromRootAndRename(builder, "treasure-maps-find-already-discovered", "find-already-discovered", "environment", "treasure-maps");
+        moveFromRoot(builder, "water-over-lava-flow-speed", "environment");
+        moveFromRoot(builder, "nether-ceiling-void-damage-height", "environment");
+
+        moveFromRoot(builder, "keep-spawn-loaded", "spawn");
+        moveFromRoot(builder, "keep-spawn-loaded-range", "spawn");
+        moveFromRoot(builder, "allow-using-signs-inside-spawn-protection", "spawn");
+
+        moveFromRoot(builder, "max-entity-collisions", "collisions");
+        moveFromRoot(builder, "allow-vehicle-collisions", "collisions");
+        moveFromRoot(builder, "fix-climbing-bypassing-cramming-rule", "collisions");
+        moveFromRoot(builder, "only-players-collide", "collisions");
+        moveFromRoot(builder, "allow-player-cramming-damage", "collisions");
+
+        moveFromRoot(builder, "falling-block-height-nerf", "fixes");
+        moveFromRoot(builder, "fix-items-merging-through-walls", "fixes");
+        moveFromRoot(builder, "prevent-tnt-from-moving-in-water", "fixes");
+        moveFromRoot(builder, "remove-corrupt-tile-entities", "fixes");
+        moveFromRoot(builder, "split-overstacked-loot", "fixes");
+        moveFromRoot(builder, "tnt-entity-height-nerf", "fixes");
+        moveFromRoot(builder, "fix-wither-targeting-bug", "fixes");
+        moveFromGameMechanics(builder, "disable-unloaded-chunk-enderpearl-exploit", "fixes");
+        moveFromGameMechanics(builder, "fix-curing-zombie-villager-discount-exploit", "fixes");
+
+        builder.addAction(path("fishing-time-range", "MaximumTicks"), TransformAction.rename("maximum"));
+        builder.addAction(path("fishing-time-range", "MinimumTicks"), TransformAction.rename("minimum"));
+
+        builder.addAction(path("generator-settings", "flat-bedrock"), (path, value) -> new Object[]{"environment", "generate-flat-bedrock"});
+        builder.addAction(path("generator-settings"), TransformAction.remove());
+
+        builder.addAction(path("game-mechanics", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> new Object[]{"misc", path.array()[1]});
+        builder.addAction(path("game-mechanics"), TransformAction.remove());
+
+        builder.addAction(path("feature-seeds", ConfigurationTransformation.WILDCARD_OBJECT), (path, value) -> {
+            final String key = path.array()[path.size() - 1].toString();
+            if (!key.equals("generate-random-seeds-for-all")) {
+                return new Object[]{"feature-seeds", "features", key};
+            }
+            return null;
+        });
+
+        builder.addAction(path("duplicate-uuid-resolver"), (path, value) -> {
+            final WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode duplicateUUIDMode = switch (value.require(String.class)) {
+                case "regen", "regenerate", "saferegen", "saferegenerate" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN;
+                case "remove", "delete" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE;
+                case "silent", "nothing" -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.NOTHING;
+                default -> WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN;
+            };
+            value.set(duplicateUUIDMode);
+            return new Object[]{"entities", "spawning", "duplicate-uuid", "mode"};
+        });
+
+        builder.addAction(path("redstone-implementation"), (path, value) -> {
+            if (value.require(String.class).equalsIgnoreCase("alternate-current")) {
+                value.set("alternate_current");
+            }
+            return new Object[]{"misc", "redstone-implementation"};
+        });
+
+        moveToMisc(builder, "light-queue-size");
+        moveToMisc(builder, "update-pathfinding-on-block-update");
+        moveToMisc(builder, "show-sign-click-command-failure-msgs-to-player");
+        moveToMisc(builder, "max-leash-distance");
+
+        return builder.build();
+    }
+
+    private static void moveToMisc(final ConfigurationTransformation.Builder builder, String... key) {
+        moveFromRootAndRename(builder, path((Object[]) key), key[key.length - 1], "misc");
+    }
+
+    private static void moveFromGameMechanics(final ConfigurationTransformation.Builder builder, final String key, final String... parents) {
+        moveFromGameMechanicsAndRename(builder, key, key, parents);
+    }
+
+    private static void moveFromGameMechanicsAndRename(final ConfigurationTransformation.Builder builder, final String oldKey, final String newKey, final String... parents) {
+        moveFromRootAndRename(builder, path("game-mechanics", oldKey), newKey, parents);
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java
new file mode 100644
index 0000000000000000000000000000000000000000..3e422b74a377fa3edaf82dd960e7449c998c2912
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/BooleanOrDefault.java
@@ -0,0 +1,53 @@
+package io.papermc.paper.configuration.type;
+
+import org.apache.commons.lang3.BooleanUtils;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.Locale;
+import java.util.function.Predicate;
+
+public record BooleanOrDefault(@Nullable Boolean value) {
+    private static final String DEFAULT_VALUE = "default";
+    public static final BooleanOrDefault USE_DEFAULT = new BooleanOrDefault(null);
+    public static final ScalarSerializer<BooleanOrDefault> SERIALIZER = new Serializer();
+
+    public boolean or(boolean fallback) {
+        return this.value != null && this.value;
+    }
+
+    private static final class Serializer extends ScalarSerializer<BooleanOrDefault> {
+        Serializer() {
+            super(BooleanOrDefault.class);
+        }
+
+        @Override
+        public BooleanOrDefault deserialize(Type type, Object obj) throws SerializationException {
+            if (obj instanceof String string) {
+                if (DEFAULT_VALUE.equalsIgnoreCase(string)) {
+                    return USE_DEFAULT;
+                }
+                try {
+                    return new BooleanOrDefault(BooleanUtils.toBoolean(string.toLowerCase(Locale.ENGLISH), "true", "false"));
+                } catch (IllegalArgumentException ex) {
+                    throw new SerializationException(BooleanOrDefault.class, obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'", ex);
+                }
+            } else if (obj instanceof Boolean bool) {
+                return new BooleanOrDefault(bool);
+            }
+            throw new SerializationException(obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'");
+        }
+
+        @Override
+        protected Object serialize(BooleanOrDefault item, Predicate<Class<?>> typeSupported) {
+            final @Nullable Boolean value = item.value;
+            if (value != null) {
+                return value.toString();
+            } else {
+                return DEFAULT_VALUE;
+            }
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java
new file mode 100644
index 0000000000000000000000000000000000000000..193709f1d08e489fc51cbe11d432529768ac1449
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java
@@ -0,0 +1,65 @@
+package io.papermc.paper.configuration.type;
+
+import org.apache.commons.lang3.math.NumberUtils;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.OptionalDouble;
+import java.util.function.Predicate;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public final class DoubleOrDefault {
+    private static final String DEFAULT_VALUE = "default";
+    public static final DoubleOrDefault USE_DEFAULT = new DoubleOrDefault(OptionalDouble.empty());
+    public static final ScalarSerializer<DoubleOrDefault> SERIALIZER = new Serializer();
+
+    private OptionalDouble value;
+
+    public DoubleOrDefault(final OptionalDouble value) {
+        this.value = value;
+    }
+
+    public OptionalDouble value() {
+        return this.value;
+    }
+
+    public void value(final OptionalDouble value) {
+        this.value = value;
+    }
+
+    public double or(final double fallback) {
+        return this.value.orElse(fallback);
+    }
+
+    private static final class Serializer extends ScalarSerializer<DoubleOrDefault> {
+        Serializer() {
+            super(DoubleOrDefault.class);
+        }
+
+        @Override
+        public DoubleOrDefault deserialize(final Type type, final Object obj) throws SerializationException {
+            if (obj instanceof String string) {
+                if (DEFAULT_VALUE.equalsIgnoreCase(string)) {
+                    return USE_DEFAULT;
+                }
+                if (NumberUtils.isParsable(string)) {
+                    return new DoubleOrDefault(OptionalDouble.of(Double.parseDouble(string)));
+                }
+            } else if (obj instanceof Number num) {
+                return new DoubleOrDefault(OptionalDouble.of(num.doubleValue()));
+            }
+            throw new SerializationException(obj + "(" + type + ") is not a double or '" + DEFAULT_VALUE + "'");
+        }
+
+        @Override
+        protected Object serialize(final DoubleOrDefault item, final Predicate<Class<?>> typeSupported) {
+            final OptionalDouble value = item.value();
+            if (value.isPresent()) {
+                return value.getAsDouble();
+            } else {
+                return DEFAULT_VALUE;
+            }
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/Duration.java b/src/main/java/io/papermc/paper/configuration/type/Duration.java
new file mode 100644
index 0000000000000000000000000000000000000000..fdc906b106a5c6fff2675d5399650f5b793deb70
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/Duration.java
@@ -0,0 +1,97 @@
+package io.papermc.paper.configuration.type;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+public final class Duration {
+
+    private static final Pattern SPACE = Pattern.compile(" ");
+    private static final Pattern NOT_NUMERIC = Pattern.compile("[^-\\d.]");
+    public static final Serializer SERIALIZER = new Serializer();
+
+    private final long seconds;
+    private final String value;
+
+    private Duration(String value) {
+        this.value = value;
+        this.seconds = getSeconds(value);
+    }
+
+    public long seconds() {
+        return this.seconds;
+    }
+
+    public long ticks() {
+        return this.seconds * 20;
+    }
+
+    public String value() {
+        return this.value;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Duration duration = (Duration) o;
+        return seconds == duration.seconds && this.value.equals(duration.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.seconds, this.value);
+    }
+
+    @Override
+    public String toString() {
+        return "Duration{" +
+                "seconds=" + this.seconds +
+                ", value='" + this.value + '\'' +
+                '}';
+    }
+
+    public static Duration of(String time) {
+        return new Duration(time);
+    }
+
+    private static int getSeconds(String str) {
+        str = SPACE.matcher(str).replaceAll("");
+        final char unit = str.charAt(str.length() - 1);
+        str = NOT_NUMERIC.matcher(str).replaceAll("");
+        double num;
+        try {
+            num = Double.parseDouble(str);
+        } catch (Exception e) {
+            num = 0D;
+        }
+        switch (unit) {
+            case 'd': num *= (double) 60*60*24; break;
+            case 'h': num *= (double) 60*60; break;
+            case 'm': num *= (double) 60; break;
+            default: case 's': break;
+        }
+        return (int) num;
+    }
+
+    private static final class Serializer extends ScalarSerializer<Duration> {
+        private Serializer() {
+            super(Duration.class);
+        }
+
+        @Override
+        public Duration deserialize(Type type, Object obj) throws SerializationException {
+            return new Duration(obj.toString());
+        }
+
+        @Override
+        protected Object serialize(Duration item, Predicate<Class<?>> typeSupported) {
+            return item.value();
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/EngineMode.java b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java
new file mode 100644
index 0000000000000000000000000000000000000000..99e90636051fa0c770ee2eafb7f0d29c8195f9ae
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java
@@ -0,0 +1,37 @@
+package io.papermc.paper.configuration.type;
+
+import io.papermc.paper.configuration.serializer.EngineModeSerializer;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+
+public enum EngineMode {
+
+    HIDE(1, "hide ores"), OBFUSCATE(2, "obfuscate");
+
+    public static final ScalarSerializer<EngineMode> SERIALIZER = new EngineModeSerializer();
+
+    private final int id;
+    private final String description;
+
+    EngineMode(int id, String description) {
+        this.id = id;
+        this.description = description;
+    }
+
+    public static EngineMode valueOf(int id) {
+        for (EngineMode engineMode : values()) {
+            if (engineMode.getId() == id) {
+                return engineMode;
+            }
+        }
+
+        throw new IllegalArgumentException("No enum constant with id " + id);
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java
new file mode 100644
index 0000000000000000000000000000000000000000..3278045dbf081cc4099e2eac3a6c4fac3012be4b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java
@@ -0,0 +1,56 @@
+package io.papermc.paper.configuration.type;
+
+import com.mojang.logging.LogUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.slf4j.Logger;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.OptionalInt;
+import java.util.function.Predicate;
+
+public record IntOrDefault(OptionalInt value) {
+    private static final String DEFAULT_VALUE = "default";
+    private static final Logger LOGGER = LogUtils.getLogger();
+    public static final IntOrDefault USE_DEFAULT = new IntOrDefault(OptionalInt.empty());
+    public static final ScalarSerializer<IntOrDefault> SERIALIZER = new Serializer();
+
+    public int or(final int fallback) {
+        return this.value.orElse(fallback);
+    }
+
+    private static final class Serializer extends ScalarSerializer<IntOrDefault> {
+        Serializer() {
+            super(IntOrDefault.class);
+        }
+
+        @Override
+        public IntOrDefault deserialize(final Type type, final Object obj) throws SerializationException {
+            if (obj instanceof String string) {
+                if (DEFAULT_VALUE.equalsIgnoreCase(string)) {
+                    return USE_DEFAULT;
+                }
+                if (NumberUtils.isParsable(string)) {
+                    return new IntOrDefault(OptionalInt.of(Integer.parseInt(string)));
+                }
+            } else if (obj instanceof Number num) {
+                if (num.intValue() != num.doubleValue() || num.intValue() != num.longValue()) {
+                    LOGGER.error("{} cannot be converted to an integer without losing information", num);
+                }
+                return new IntOrDefault(OptionalInt.of(num.intValue()));
+            }
+            throw new SerializationException(obj + "(" + type + ") is not a integer or '" + DEFAULT_VALUE + "'");
+        }
+
+        @Override
+        protected Object serialize(final IntOrDefault item, final Predicate<Class<?>> typeSupported) {
+            final OptionalInt value = item.value();
+            if (value.isPresent()) {
+                return value.getAsInt();
+            } else {
+                return DEFAULT_VALUE;
+            }
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java b/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java
new file mode 100644
index 0000000000000000000000000000000000000000..24763d3d270c29c95e0b3e85111145234f660a62
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java
@@ -0,0 +1,38 @@
+package io.papermc.paper.configuration.type.fallback;
+
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.util.Map;
+import java.util.OptionalInt;
+import java.util.Set;
+
+public class ArrowDespawnRate extends FallbackValue.Int {
+
+    ArrowDespawnRate(Map<ContextKey<?>, Object> context, Object value) throws SerializationException {
+        super(context, fromObject(value));
+    }
+
+    private ArrowDespawnRate(Map<ContextKey<?>, Object> context) {
+        super(context, OptionalInt.empty());
+    }
+
+    @Override
+    protected OptionalInt process(int value) {
+        return Util.negToDef(value);
+    }
+
+    @Override
+    public Set<ContextKey<?>> required() {
+        return Set.of(FallbackValue.SPIGOT_WORLD_CONFIG);
+    }
+
+    @Override
+    protected int fallback() {
+        return this.get(FallbackValue.SPIGOT_WORLD_CONFIG).arrowDespawnRate;
+    }
+
+    public static ArrowDespawnRate def(SpigotWorldConfig spigotConfig) {
+        return new ArrowDespawnRate(FallbackValue.SPIGOT_WORLD_CONFIG.singleton(spigotConfig));
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java b/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java
new file mode 100644
index 0000000000000000000000000000000000000000..0f2765b2edc63c11ba3c57ff55c536054826a995
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/fallback/AutosavePeriod.java
@@ -0,0 +1,39 @@
+package io.papermc.paper.configuration.type.fallback;
+
+import net.minecraft.server.MinecraftServer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.util.Map;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.function.Supplier;
+
+public class AutosavePeriod extends FallbackValue.Int {
+
+    AutosavePeriod(Map<ContextKey<?>, Object> contextMap, Object value) throws SerializationException {
+        super(contextMap, fromObject(value));
+    }
+
+    private AutosavePeriod(Map<ContextKey<?>, Object> contextMap) {
+        super(contextMap, OptionalInt.empty());
+    }
+
+    @Override
+    protected OptionalInt process(int value) {
+        return Util.negToDef(value);
+    }
+
+    @Override
+    protected Set<ContextKey<?>> required() {
+        return Set.of(FallbackValue.MINECRAFT_SERVER);
+    }
+
+    @Override
+    protected int fallback() {
+        return this.get(FallbackValue.MINECRAFT_SERVER).get().autosavePeriod;
+    }
+
+    public static AutosavePeriod def() {
+        return new AutosavePeriod(FallbackValue.MINECRAFT_SERVER.singleton(MinecraftServer::getServer));
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java
new file mode 100644
index 0000000000000000000000000000000000000000..a3a1d398d783c37914fb6d646e11361afee687b8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java
@@ -0,0 +1,102 @@
+package io.papermc.paper.configuration.type.fallback;
+
+import com.google.common.base.Preconditions;
+import net.minecraft.server.MinecraftServer;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.function.Supplier;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public sealed abstract class FallbackValue permits FallbackValue.Int {
+
+    private static final String DEFAULT_VALUE = "default";
+    static final ContextKey<SpigotWorldConfig> SPIGOT_WORLD_CONFIG = new ContextKey<>("SpigotWorldConfig");
+    static final ContextKey<Supplier<MinecraftServer>> MINECRAFT_SERVER = new ContextKey<>("MinecraftServer");
+
+    private final Map<ContextKey<?>, Object> contextMap;
+
+    protected FallbackValue(Map<ContextKey<?>, Object> contextMap) {
+        for (ContextKey<?> contextKey : this.required()) {
+            Preconditions.checkArgument(contextMap.containsKey(contextKey), contextMap + " is missing " + contextKey);
+        }
+        this.contextMap = contextMap;
+    }
+
+    protected abstract String serialize();
+
+    protected abstract Set<ContextKey<?>> required();
+
+    @SuppressWarnings("unchecked")
+    protected <T> T get(ContextKey<T> contextKey) {
+        return (T) Objects.requireNonNull(this.contextMap.get(contextKey), "Missing " + contextKey);
+    }
+
+    public non-sealed abstract static class Int extends FallbackValue {
+
+        private final OptionalInt value;
+
+        Int(Map<ContextKey<?>, Object> contextMap, OptionalInt value) {
+            super(contextMap);
+            if (value.isEmpty()) {
+                this.value = value;
+            } else {
+                this.value = this.process(value.getAsInt());
+            }
+        }
+
+        public int value() {
+            return value.orElseGet(this::fallback);
+        }
+
+        @Override
+        protected final String serialize() {
+            return value.isPresent() ? String.valueOf(this.value.getAsInt()) : DEFAULT_VALUE;
+        }
+
+        protected OptionalInt process(int value) {
+            return OptionalInt.of(value);
+        }
+
+        protected abstract int fallback();
+
+        protected static OptionalInt fromObject(Object obj) throws SerializationException {
+            if (obj instanceof OptionalInt optionalInt) {
+                return optionalInt;
+            } else if (obj instanceof String string) {
+                if (DEFAULT_VALUE.equalsIgnoreCase(string)) {
+                    return OptionalInt.empty();
+                }
+                if (NumberUtils.isParsable(string)) {
+                    return OptionalInt.of(Integer.parseInt(string));
+                }
+            } else if (obj instanceof Integer num) {
+                return OptionalInt.of(num);
+            }
+            throw new SerializationException(obj + " is not a integer or '" + DEFAULT_VALUE + "'");
+        }
+    }
+
+    static class ContextKey<T> {
+
+        private final String name;
+
+        ContextKey(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public String toString() {
+            return this.name;
+        }
+
+        Map<ContextKey<?>, Object> singleton(T value) {
+            return Map.of(this, value);
+        }
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d0fcd038e12c70a3a5aaf2669452589d9055255
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValueSerializer.java
@@ -0,0 +1,55 @@
+package io.papermc.paper.configuration.type.fallback;
+
+import net.minecraft.server.MinecraftServer;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spigotmc.SpigotWorldConfig;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import static io.leangen.geantyref.GenericTypeReflector.erase;
+
+public class FallbackValueSerializer extends ScalarSerializer<FallbackValue> {
+
+    private static final Map<Class<?>, FallbackCreator<?>> REGISTRY = new HashMap<>();
+
+    static {
+        REGISTRY.put(ArrowDespawnRate.class, ArrowDespawnRate::new);
+        REGISTRY.put(AutosavePeriod.class, AutosavePeriod::new);
+    }
+
+    FallbackValueSerializer(Map<FallbackValue.ContextKey<?>, Object> contextMap) {
+        super(FallbackValue.class);
+        this.contextMap = contextMap;
+    }
+
+    @FunctionalInterface
+    private interface FallbackCreator<T extends FallbackValue> {
+        T create(Map<FallbackValue.ContextKey<?>, Object> context, Object value) throws SerializationException;
+    }
+
+    private final Map<FallbackValue.ContextKey<?>, Object> contextMap;
+
+    @Override
+    public FallbackValue deserialize(Type type, Object obj) throws SerializationException {
+        final @Nullable FallbackCreator<?> creator = REGISTRY.get(erase(type));
+        if (creator == null) {
+            throw new SerializationException(type + " does not have a FallbackCreator registered");
+        }
+        return creator.create(this.contextMap, obj);
+    }
+
+    @Override
+    protected Object serialize(FallbackValue item, Predicate<Class<?>> typeSupported) {
+        return item.serialize();
+    }
+
+    public static FallbackValueSerializer create(SpigotWorldConfig config, Supplier<MinecraftServer> server) {
+        return new FallbackValueSerializer(Map.of(FallbackValue.SPIGOT_WORLD_CONFIG, config, FallbackValue.MINECRAFT_SERVER, server));
+    }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java b/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java
new file mode 100644
index 0000000000000000000000000000000000000000..70cc7b45e7355f6c8476a74a070f1266e4cca189
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/fallback/Util.java
@@ -0,0 +1,10 @@
+package io.papermc.paper.configuration.type.fallback;
+
+import java.util.OptionalInt;
+
+final class Util {
+
+    static OptionalInt negToDef(int value) {
+        return value < 0 ?  OptionalInt.empty() : OptionalInt.of(value);
+    }
+}
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
index 853e7c2019f5147e9681e95a82eaef0825b6341e..a48a12a31a3d09a9373b688dcc093035f8f8a300 100644
--- a/src/main/java/net/minecraft/server/Main.java
+++ b/src/main/java/net/minecraft/server/Main.java
@@ -110,6 +110,11 @@ public class Main {
             DedicatedServerSettings dedicatedserversettings = new DedicatedServerSettings(optionset); // CraftBukkit - CLI argument support
 
             dedicatedserversettings.forceSave();
+            // Paper start - load config files for access below if needed
+            org.bukkit.configuration.file.YamlConfiguration bukkitConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("bukkit-settings"));
+            org.bukkit.configuration.file.YamlConfiguration spigotConfiguration = io.papermc.paper.configuration.PaperConfigurations.loadLegacyConfigFile((File) optionset.valueOf("spigot-settings"));
+            // Paper end
+
             Path path1 = Paths.get("eula.txt");
             Eula eula = new Eula(path1);
 
@@ -133,7 +138,7 @@ public class Main {
             }
 
             File file = (File) optionset.valueOf("universe"); // CraftBukkit
-            Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file);
+            Services services = Services.create(new YggdrasilAuthenticationService(Proxy.NO_PROXY), file, optionset); // Paper
             // CraftBukkit start
             String s = (String) Optional.ofNullable((String) optionset.valueOf("world")).orElse(dedicatedserversettings.getProperties().levelName);
             LevelStorageSource convertable = LevelStorageSource.createDefault(file.toPath());
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index e7b1456a123208241d0b1c5956a137d6a5cfbfcd..7740e69617c3d543a67ed0942ba8ec550ad4386d 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -281,6 +281,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
     private static final int SAMPLE_INTERVAL = 100;
     public final double[] recentTps = new double[ 3 ];
     // Spigot end
+    public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations;
 
     public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
         AtomicReference<S> atomicreference = new AtomicReference();
@@ -371,6 +372,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
             }
         }
         Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
+        this.paperConfigurations = services.paperConfigurations(); // Paper
     }
     // CraftBukkit end
 
diff --git a/src/main/java/net/minecraft/server/Services.java b/src/main/java/net/minecraft/server/Services.java
index 697ca7457115423a8c4d8a7d1f7a353237b56509..d7d65d0faefa5551480a4090de3a881828238ffd 100644
--- a/src/main/java/net/minecraft/server/Services.java
+++ b/src/main/java/net/minecraft/server/Services.java
@@ -7,14 +7,30 @@ import java.io.File;
 import net.minecraft.server.players.GameProfileCache;
 import net.minecraft.util.SignatureValidator;
 
-public record Services(MinecraftSessionService sessionService, SignatureValidator serviceSignatureValidator, GameProfileRepository profileRepository, GameProfileCache profileCache) {
+// Paper start
+public record Services(MinecraftSessionService sessionService, SignatureValidator serviceSignatureValidator, GameProfileRepository profileRepository, GameProfileCache profileCache, @javax.annotation.Nullable io.papermc.paper.configuration.PaperConfigurations paperConfigurations) {
+
+    public Services(MinecraftSessionService sessionService, SignatureValidator signatureValidator, GameProfileRepository profileRepository, GameProfileCache profileCache) {
+        this(sessionService, signatureValidator, profileRepository, profileCache, null);
+    }
+
+    @Override
+    public io.papermc.paper.configuration.PaperConfigurations paperConfigurations() {
+        return java.util.Objects.requireNonNull(this.paperConfigurations);
+    }
+    // Paper end
     private static final String USERID_CACHE_FILE = "usercache.json";
 
-    public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory) {
+    public static Services create(YggdrasilAuthenticationService authenticationService, File rootDirectory, joptsimple.OptionSet optionSet) throws Exception { // Paper
         MinecraftSessionService minecraftSessionService = authenticationService.createMinecraftSessionService();
         GameProfileRepository gameProfileRepository = authenticationService.createProfileRepository();
         GameProfileCache gameProfileCache = new GameProfileCache(gameProfileRepository, new File(rootDirectory, "usercache.json"));
         SignatureValidator signatureValidator = SignatureValidator.from(authenticationService.getServicesKey());
-        return new Services(minecraftSessionService, signatureValidator, gameProfileRepository, gameProfileCache);
+        // Paper start
+        final java.nio.file.Path legacyConfigPath = ((File) optionSet.valueOf("paper-settings")).toPath();
+        final java.nio.file.Path configDirPath = ((File) optionSet.valueOf("paper-settings-directory")).toPath();
+        io.papermc.paper.configuration.PaperConfigurations paperConfigurations = io.papermc.paper.configuration.PaperConfigurations.setup(legacyConfigPath, configDirPath, rootDirectory.toPath(), (File) optionSet.valueOf("spigot-settings"));
+        return new Services(minecraftSessionService, signatureValidator, gameProfileRepository, gameProfileCache, paperConfigurations);
+        // Paper end
     }
 }
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 7d965247833c91dc824e6cc56e8b0fe5f3413d1d..08ae7a96e93c0d8547f560b3f753804525621c6b 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -188,6 +188,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
         org.spigotmc.SpigotConfig.init((java.io.File) options.valueOf("spigot-settings"));
         org.spigotmc.SpigotConfig.registerCommands();
         // Spigot end
+        // Paper start
+        paperConfigurations.initializeGlobalConfiguration();
+        paperConfigurations.initializeWorldDefaultsConfiguration();
+        // Paper end
 
         this.setPvpAllowed(dedicatedserverproperties.pvp);
         this.setFlightAllowed(dedicatedserverproperties.allowFlight);
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 3f4dbd3503bc3224a2f483563af25a76cc0b68f9..1a23437b9fa17846fd28163ae930d21a6bb00138 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -227,7 +227,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
     public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
         // Holder holder = worlddimension.typeHolder(); // CraftBukkit - decompile error
         // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error
-        super(iworlddataserver, resourcekey, worlddimension.typeHolder(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env);
+        super(iworlddataserver, resourcekey, worlddimension.typeHolder(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig))); // Paper
         this.pvpMode = minecraftserver.isPvpAllowed();
         this.convertable = convertable_conversionsession;
         this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile());
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index c1194f459414dc6ca9626ab8cec48cb48cdd926b..649df119b24dc8c390f45e9f813cf8c37994e0cf 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -149,6 +149,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
     public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
     public boolean populating;
     public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
+    // Paper start
+    private final io.papermc.paper.configuration.WorldConfiguration paperConfig;
+    public io.papermc.paper.configuration.WorldConfiguration paperConfig() {
+        return this.paperConfig;
+    }
+    // Paper end
 
     public final SpigotTimings.WorldTimingsHandler timings; // Spigot
     public static BlockPos lastPhysicsProblem; // Spigot
@@ -166,8 +172,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
 
     public abstract ResourceKey<LevelStem> getTypeKey();
 
-    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env) {
+    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper
         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
+        this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper
         this.generator = gen;
         this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env);
 
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index dbaec6fc4967d8140fd5af68456894bad1a0205a..4f67f7541ff257f35701610760fb8e04e8379fe3 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -870,6 +870,7 @@ public final class CraftServer implements Server {
         }
 
         org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot
+        this.console.paperConfigurations.reloadConfigs(this.console);
         for (ServerLevel world : this.console.getAllLevels()) {
             world.serverLevelData.setDifficulty(config.difficulty);
             world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals);
diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
index f264607eb3df1f247c7c36d328405900b52df38e..b05cea49219c6582bacc705f41e72ca7c7eb6a8c 100644
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
@@ -129,6 +129,19 @@ public class Main {
                         .defaultsTo(new File("spigot.yml"))
                         .describedAs("Yml file");
                 // Spigot End
+
+                // Paper Start
+                acceptsAll(asList("paper-dir", "paper-settings-directory"), "Directory for Paper settings")
+                    .withRequiredArg()
+                    .ofType(File.class)
+                    .defaultsTo(new File(io.papermc.paper.configuration.PaperConfigurations.CONFIG_DIR))
+                    .describedAs("Config directory");
+                acceptsAll(asList("paper", "paper-settings"), "File for Paper settings")
+                        .withRequiredArg()
+                        .ofType(File.class)
+                        .defaultsTo(new File("paper.yml"))
+                        .describedAs("Yml file");
+                // Paper end
             }
         };
 
diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java
index a96cb7a5f7c94cd9a46b31cf8ec90b544221557b..7c35d86eac0d69ba4be48faf364fd6dc84fa7e87 100644
--- a/src/main/java/org/spigotmc/SpigotConfig.java
+++ b/src/main/java/org/spigotmc/SpigotConfig.java
@@ -96,7 +96,7 @@ public class SpigotConfig
         }
     }
 
-    static void readConfig(Class<?> clazz, Object instance)
+    public static void readConfig(Class<?> clazz, Object instance) // Paper - package-private -> public
     {
         for ( Method method : clazz.getDeclaredMethods() )
         {
diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java
index bd0bf398f900302187f3436119c754592d575416..d139cbcf0b159372f229bef6ae49b45a74c163ad 100644
--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java
+++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java
@@ -58,8 +58,14 @@ public class SpigotWorldConfig
 
     public int getInt(String path, int def)
     {
-        this.config.addDefault( "world-settings.default." + path, def );
-        return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path ) );
+        // Paper start - get int without setting default
+        return this.getInt(path, def, true);
+    }
+    public int getInt(String path, int def, boolean setDef)
+    {
+        if (setDef) this.config.addDefault( "world-settings.default." + path, def );
+        return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path, def ) );
+        // Paper end
     }
 
     public <T> List getList(String path, T def)
diff --git a/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..0396589795da1f83ddf62426236dde9a3afa1376
--- /dev/null
+++ b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java
@@ -0,0 +1,20 @@
+package io.papermc.paper.configuration;
+
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+public final class GlobalConfigTestingBase {
+
+    public static void setupGlobalConfigForTest() {
+        //noinspection ConstantConditions
+        if (GlobalConfiguration.get() == null) {
+            ConfigurationNode node = PaperConfigurations.createForTesting();
+            try {
+                GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class);
+                GlobalConfiguration.set(globalConfiguration);
+            } catch (SerializationException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+}
diff --git a/src/test/java/org/bukkit/support/AbstractTestingBase.java b/src/test/java/org/bukkit/support/AbstractTestingBase.java
index 6816d8a9fa504ca5a25fa62c0f0974e3e744ead6..e73a9a957cd55bf838e301ed531295162f2cfb89 100644
--- a/src/test/java/org/bukkit/support/AbstractTestingBase.java
+++ b/src/test/java/org/bukkit/support/AbstractTestingBase.java
@@ -46,6 +46,7 @@ public abstract class AbstractTestingBase {
 
         DummyServer.setup();
         DummyEnchantments.setup();
+        io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(); // Paper
 
         ImmutableList.Builder<Material> builder = ImmutableList.builder();
         for (Material m : Material.values()) {
