Paper/patches/server/0004-Paper-config-files.patch
Nassim Jahnke fb2c24b36d
Updated Upstream (Bukkit/CraftBukkit) (#8015)
Upstream has released updates that appear to apply and compile correctly.
This update has not been tested by PaperMC and as with ANY update, please do your own testing

Bukkit Changes:
05ae036c PR-746: Add option to use cached map color palette
57849c1b PR-759: Add preview chat option in ServerListPingEvent
0169e65d PR-758: Add missing server properties methods from 1.19

CraftBukkit Changes:
622dbe6c2 SPIGOT-7068: SKULK and SKULK_VEIN BlockSpreadEvents Still do not reference the correct source (SKULK_CATALYST)
6c61b73f3 PR-1052: Add option to use cached map color palette
c882f38ea SPIGOT-7066: Fix custom END worlds not generating DragonBattle
6866aab59 SPIGOT-2420: Can't set exp drops for EnderDragon death
9dcd46530 PR-1067: Add preview chat option in ServerListPingEvent
36c2681af PR-1066: Add missing server properties methods from 1.19
031eaadd0 Increase outdated build delay
8fda4b12f SPIGOT-7060: SCULK and SCULK_VEIN BlockSpreadEvents do not reference the correct source
2022-06-20 19:12:05 +02:00

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