3
0
Mirror von https://github.com/PaperMC/Paper.git synchronisiert 2024-11-15 04:20:04 +01:00
Paper/patches/server/0004-Paper-config-files.patch
2022-06-10 14:58:25 +02:00

4217 Zeilen
198 KiB
Diff

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 35839e1c2dede1e3a1e54b124667cf2bf63e73f0..a87cf2a4ad955b7429269f2d46adbe59f6e4525e 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..cb7d11dcf13c6ac464634a7e8115bf3dee0e72a2
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java
@@ -0,0 +1,26 @@
+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)
+ .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..13d7d1c24ec9192d0163f6eedeac8fca82b6e80c
--- /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)) {
+ Files.createDirectories(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..5a808a09291da691cbee75a55f6aa1b70ac9f018
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -0,0 +1,264 @@
+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(@Constraint(Constraints.Positive.class) @Required double interval, @Constraint(Constraints.Positive.class) @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;
+ }
+}
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..b33c7e67cff07c801d1b7aa2bc342ab5a9c1bad3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java
@@ -0,0 +1,118 @@
+package io.papermc.paper.configuration;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spigotmc.SpigotWorldConfig;
+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.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();
+ }
+
+ @Override
+ public void complete(Object instance, Map<Field, Object> intermediate) throws SerializationException {
+ mutableInstanceFactoryDelegate.complete(instance, intermediate);
+ }
+
+ @Override
+ public Object complete(Map<Field, Object> intermediate) throws SerializationException {
+ Object value = mutableInstanceFactoryDelegate.complete(intermediate);
+ if (value instanceof ConfigurationPart.Post post) {
+ post.postProcess();
+ }
+ return value;
+ }
+
+ @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/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..be64437ba7630fe069aaea502932b514c85162f8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
@@ -0,0 +1,384 @@
+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.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.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";
+ private static final Path BACKUP_DIR = Path.of("legacy-backup");
+
+ private static final String GLOBAL_HEADER = """
+ 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 to their own files.
+
+ Discord: https://discord.gg/papermc
+ Website: https://papermc.io/
+ Docs: https://docs.papermc.io/""";
+
+ 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.
+
+ Discord: https://discord.gg/papermc
+ Website: https://papermc.io/
+ Docs: https://docs.papermc.io/""";
+
+ private static final String WORLD_HEADER = """
+ This is a world configuration file for Paper.
+ This file may start empty but can be filled with settings to override ones in the config/world-defaults.yml""";
+
+ 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)
+ .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(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 {
+ if (needsConverting(legacyConfig)) {
+ try {
+ Files.createDirectories(configDir.resolve(BACKUP_DIR));
+ final Path legacyConfigBackup = configDir.resolve(BACKUP_DIR).resolve(legacyConfig.getFileName().toString() + ".old");
+ Files.move(legacyConfig, legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup
+ convert(legacyConfigBackup, configDir, worldFolder, spigotConfig);
+ } catch (final IOException ex) {
+ throw new RuntimeException("Could not convert paper.yml to the new configuration format", ex);
+ }
+ }
+ try {
+ Files.createDirectories(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 {
+ Files.createDirectories(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);
+ }
+}
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..d69d203eea014fc9fb40a556f0771dba15fcd2ea
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java
@@ -0,0 +1,61 @@
+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"),
+ };
+
+ 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..5bcce63166657f80eaa8446d3dd64a5cba62f198
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java
@@ -0,0 +1,465 @@
+package io.papermc.paper.configuration;
+
+import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray;
+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.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 ChunkPacketBlockControllerAntiXray.EngineMode engineMode = ChunkPacketBlockControllerAntiXray.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;
+ public Reference2IntMap<MobCategory> spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1)));
+ 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/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..0b235ebe6e79d7aa420d6b8a52aedb3a4d8b6629
--- /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()) {
+ rowKeySerializer.deserialize(rowType, rowKeyNode.set(key));
+ final Map<C, V> map = columnValueSerializer.deserialize(mapType, node.node(rowKeyNode.raw()));
+ map.forEach((column, value) -> table.put((R) rowKeyNode.raw(), 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..37aa2873eb7fbfb9cfbf890e5ca2a3965f8d612f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java
@@ -0,0 +1,64 @@
+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;
+
+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 Double num) {
+ return new DoubleOrDefault(OptionalDouble.of(num));
+ }
+ throw new SerializationException(obj + " is of an unexpected type " + type);
+ }
+
+ @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/IntOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java
new file mode 100644
index 0000000000000000000000000000000000000000..18a77c5694dc9739c7e2b52deb7dbfebb01b6c38
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java
@@ -0,0 +1,49 @@
+package io.papermc.paper.configuration.type;
+
+import java.lang.reflect.Type;
+import java.util.OptionalInt;
+import java.util.function.Predicate;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.spongepowered.configurate.serialize.ScalarSerializer;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+public record IntOrDefault(OptionalInt value) {
+ private static final String DEFAULT_VALUE = "default";
+ 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 Integer num) {
+ return new IntOrDefault(OptionalInt.of(num));
+ }
+ 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..d279ef02a44e2a8fc971cbf8ec816444271ccfc9
--- /dev/null
+++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java
@@ -0,0 +1,101 @@
+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;
+
+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 6cc81495d8d09ff1fbb09f2e63a16ec4fa6138ec..73662a463fc9d38b39c4c779fcfaa09a4689d704 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -279,6 +279,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();
@@ -369,6 +370,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..86c218e41fbe2a55b1319b37d26fd5da4f0b3da6 100644
--- a/src/main/java/net/minecraft/server/Services.java
+++ b/src/main/java/net/minecraft/server/Services.java
@@ -7,14 +7,28 @@ 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
+ io.papermc.paper.configuration.PaperConfigurations paperConfigurations = io.papermc.paper.configuration.PaperConfigurations.setup(((File) optionSet.valueOf("paper-settings")).toPath(), java.nio.file.Path.of("config"), 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 570db14d930e15a96621d0d24ce11a27dc38494b..e476f93547f386ded0174693a6218d793ccc450b 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 e45b71fa8ac1c3d0bca5f2c1737f302e787a1df3..3dadcddc9b4954e2c572e76e6b6c7d570fd47b8e 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -226,7 +226,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 1d94c0fbdead83155aefc8d4a16dbcb95b3c9838..42be8ab024e5c889b2a114f1098e1bedd1193d80 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -861,6 +861,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 e5008c75054df38356af193fd049110d7d56e2d4..c694c6dfed0b3aa098b1822676e39bd3eb04b45a 100644
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
@@ -129,6 +129,14 @@ public class Main {
.defaultsTo(new File("spigot.yml"))
.describedAs("Yml file");
// Spigot End
+
+ // Paper Start
+ 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 a04da0a7d690fe3fcf10810b4e8c92a8ae027b86..feef74e3a6d50344245c4a61ece5b2194af1072f 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 b58962026c739ed06fbe0f702cb2877fbfa7b2e4..7de91b0b669664be3406b77a453d517604170404 100644
--- a/src/test/java/org/bukkit/support/AbstractTestingBase.java
+++ b/src/test/java/org/bukkit/support/AbstractTestingBase.java
@@ -45,6 +45,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()) {