diff --git a/patches/server/0096-Paper-config-files.patch b/patches/server/0096-Paper-config-files.patch new file mode 100644 index 000000000..e8a5160d2 --- /dev/null +++ b/patches/server/0096-Paper-config-files.patch @@ -0,0 +1,4420 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Wed, 8 Jun 2022 22:20:16 -0700 +Subject: [PATCH] Paper config files + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 5d8a84341ab5be52b5c37737e3f82590f06f6073..cfdb20447e6ad3efcdee8889712f77931773beaf 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -12,6 +12,7 @@ dependencies { + implementation("org.apache.logging.log4j:log4j-iostreams:2.17.1") // Paper + implementation("org.ow2.asm:asm:9.3") + implementation("org.ow2.asm:asm-commons:9.3") // Paper - ASM event executor generation ++ implementation("org.spongepowered:configurate-yaml:4.1.2") // Paper - config files + implementation("commons-lang:commons-lang:2.6") + runtimeOnly("org.xerial:sqlite-jdbc:3.36.0.3") + runtimeOnly("mysql:mysql-connector-java:8.0.29") +diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ef41cf3a7d1e6f2bfe81e0fb865d2f969bbc77c1 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java +@@ -0,0 +1,8 @@ ++package com.destroystokyo.paper; ++ ++/** ++ * @deprecated kept as a means to identify Paper in older plugins/PaperLib ++ */ ++@Deprecated(forRemoval = true) ++public class PaperConfig { ++} +diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c91f109b4cf64dc1b4ef09f38e1cb8bf5cb2be13 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +@@ -0,0 +1,8 @@ ++package com.destroystokyo.paper; ++ ++/** ++ * @deprecated kept as a means to identify Paper in older plugins/PaperLib ++ */ ++@Deprecated(forRemoval = true) ++public class PaperWorldConfig { ++} +diff --git a/src/main/java/io/papermc/paper/configuration/Configuration.java b/src/main/java/io/papermc/paper/configuration/Configuration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..817fd26cc3591f9cae0f61f4036dde43c4ed60e8 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/Configuration.java +@@ -0,0 +1,13 @@ ++package io.papermc.paper.configuration; ++ ++public final class Configuration { ++ public static final String VERSION_FIELD = "_version"; ++ @Deprecated ++ public static final String LEGACY_CONFIG_VERSION_FIELD = "config-version"; ++ ++ @Deprecated ++ public static final int FINAL_LEGACY_VERSION = 27; ++ ++ private Configuration() { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java +new file mode 100644 +index 0000000000000000000000000000000000000000..227039a6c69c4c99bbd9c674b3aab0ef5e2c1374 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java +@@ -0,0 +1,27 @@ ++package io.papermc.paper.configuration; ++ ++import java.nio.file.Path; ++import org.spongepowered.configurate.loader.HeaderMode; ++import org.spongepowered.configurate.util.MapFactories; ++import org.spongepowered.configurate.yaml.NodeStyle; ++import org.spongepowered.configurate.yaml.YamlConfigurationLoader; ++ ++public final class ConfigurationLoaders { ++ private ConfigurationLoaders() { ++ } ++ ++ public static YamlConfigurationLoader.Builder naturallySorted() { ++ return YamlConfigurationLoader.builder() ++ .indent(2) ++ .nodeStyle(NodeStyle.BLOCK) ++ .headerMode(HeaderMode.PRESET) ++ .defaultOptions(options -> options.mapFactory(MapFactories.sortedNatural())); ++ } ++ ++ public static YamlConfigurationLoader naturallySortedWithoutHeader(final Path path) { ++ return naturallySorted() ++ .headerMode(HeaderMode.NONE) ++ .path(path) ++ .build(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7a4a7a654fe2516ed894a68f2657344df9d70f4c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java +@@ -0,0 +1,10 @@ ++package io.papermc.paper.configuration; ++ ++abstract class ConfigurationPart { ++ ++ public static abstract class Post extends ConfigurationPart { ++ ++ public abstract void postProcess(); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..31325994ab441c59a4c0bd9f3f9db3d9440375d0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/Configurations.java +@@ -0,0 +1,296 @@ ++package io.papermc.paper.configuration; ++ ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.configuration.constraint.Constraint; ++import io.papermc.paper.configuration.constraint.Constraints; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.level.ServerLevel; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.jetbrains.annotations.MustBeInvokedByOverriders; ++import org.spongepowered.configurate.CommentedConfigurationNode; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.ConfigurationOptions; ++import org.spongepowered.configurate.objectmapping.ObjectMapper; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.util.CheckedFunction; ++import org.spongepowered.configurate.yaml.YamlConfigurationLoader; ++ ++import java.io.IOException; ++import java.lang.reflect.Type; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.NoSuchElementException; ++import java.util.Objects; ++import java.util.function.UnaryOperator; ++ ++public abstract class Configurations { ++ ++ 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 globalConfigClass; ++ protected final Class worldConfigClass; ++ protected final String globalConfigFileName; ++ protected final String defaultWorldConfigFileName; ++ protected final String worldConfigFileName; ++ ++ public Configurations( ++ final Path globalFolder, ++ final Class globalConfigType, ++ final Class 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 CheckedFunction creator(Class type, boolean refreshNode) { ++ return node -> { ++ T instance = node.require(type); ++ if (refreshNode) { ++ node.set(type, instance); ++ } ++ return instance; ++ }; ++ } ++ ++ static CheckedFunction reloader(Class type, T instance) { ++ return node -> { ++ ObjectMapper.Factory factory = (ObjectMapper.Factory) Objects.requireNonNull(node.options().serializers().get(type)); ++ ObjectMapper.Mutable mutable = (ObjectMapper.Mutable) 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 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 creator) throws IOException { ++ final YamlConfigurationLoader defaultsLoader = this.createDefaultWorldLoader(true, this.createDefaultContextMap().build()).loader(); ++ final ConfigurationNode defaultsNode = defaultsLoader.load(); ++ ++ boolean newFile = false; ++ final Path dir = contextMap.require(WORLD_DIRECTORY); ++ final Path worldConfigFile = dir.resolve(this.worldConfigFileName); ++ if (Files.notExists(worldConfigFile)) { ++ PaperConfigurations.createDirectoriesSymlinkAware(dir); ++ Files.createFile(worldConfigFile); // create empty file as template ++ newFile = true; ++ } ++ ++ final YamlConfigurationLoader worldLoader = this.createWorldConfigLoaderBuilder(contextMap) ++ .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build())) ++ .path(worldConfigFile) ++ .build(); ++ final ConfigurationNode worldNode = worldLoader.load(); ++ if (newFile) { // set the version field if new file ++ worldNode.node(Configuration.VERSION_FIELD).set(WorldConfiguration.CURRENT_VERSION); ++ } ++ this.applyWorldConfigTransformations(contextMap, worldNode); ++ this.applyDefaultsAwareWorldConfigTransformations(contextMap, worldNode, defaultsNode); ++ worldLoader.save(worldNode); // save before loading node NOTE: don't save the backing node after loading it, or you'll fill up the world-specific config ++ worldNode.mergeFrom(defaultsNode); ++ return creator.apply(worldNode); ++ } ++ ++ protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException { ++ } ++ ++ protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException { ++ } ++ ++ private UnaryOperator 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, Object> backingMap; ++ ++ private ContextMap(Map, Object> map) { ++ this.backingMap = Map.copyOf(map); ++ } ++ ++ @SuppressWarnings("unchecked") ++ public T require(ContextKey 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 @Nullable T get(ContextKey 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, Object> buildingMap = new HashMap<>(); ++ ++ public Builder put(ContextKey key, T value) { ++ this.buildingMap.put(key, value); ++ return this; ++ } ++ ++ public Builder put(ContextKey key) { ++ this.buildingMap.put(key, VOID); ++ return this; ++ } ++ ++ public ContextMap build() { ++ return new ContextMap(this.buildingMap); ++ } ++ } ++ } ++ ++ public static final ContextKey WORLD_DIRECTORY = new ContextKey<>(Path.class, "world directory"); ++ public static final ContextKey WORLD_NAME = new ContextKey<>(String.class, "world name"); // TODO remove when we deprecate level names ++ public static final ContextKey WORLD_KEY = new ContextKey<>(ResourceLocation.class, "world key"); ++ public static final ContextKey FIRST_DEFAULT = new ContextKey<>(Void.class, "first default"); ++ ++ public record ContextKey(TypeToken type, String name) { ++ ++ public ContextKey(Class 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..f5100edd1bd664326d7b47b50d3a9f687ecb03d2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -0,0 +1,269 @@ ++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); ++ ++ @Nullable ++ public Component motd = null; ++ ++ 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 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>, 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> { ++ ++ private final Map, Object> instanceMap = new HashMap<>(); ++ private final Map, Object> overrides; ++ @SuppressWarnings("unchecked") ++ private final FieldDiscoverer> delegate = (FieldDiscoverer>) 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 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, Object> overrides) { ++ this.overrides = overrides; ++ } ++ ++ @Override ++ public @Nullable InstanceFactory> discover(AnnotatedType target, FieldCollector, V> collector) throws SerializationException { ++ final Class clazz = erase(target.getType()); ++ if (ConfigurationPart.class.isAssignableFrom(clazz)) { ++ final FieldDiscoverer.@Nullable InstanceFactory> instanceFactoryDelegate = this.delegate.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> mutableInstanceFactoryDelegate) { ++ return new MutableInstanceFactory<>() { ++ @Override ++ public Map begin() { ++ return mutableInstanceFactoryDelegate.begin(); ++ } ++ ++ @SuppressWarnings("unchecked") ++ @Override ++ public void complete(Object instance, Map intermediate) throws SerializationException { ++ final Iterator> iter = intermediate.entrySet().iterator(); ++ try { ++ while (iter.hasNext()) { // manually merge any mergeable maps ++ Map.Entry 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 existingMap = (Map) 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 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, Object> overrides() { ++ return this.overrides; ++ } ++ ++ static FieldDiscoverer worldConfig(Configurations.ContextMap contextMap) { ++ final Map, 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 ++ *

++ * Note that when the config is reloaded, the maps will be merged again, so make sure this map can't accumulate ++ * keys overtime. ++ */ ++@Documented ++@Target(ElementType.FIELD) ++@Retention(RetentionPolicy.RUNTIME) ++public @interface MergeMap { ++} +diff --git a/src/main/java/io/papermc/paper/configuration/NestedSetting.java b/src/main/java/io/papermc/paper/configuration/NestedSetting.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69add4a7f1147015806bc9b63a8340d1893356c1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/NestedSetting.java +@@ -0,0 +1,32 @@ ++package io.papermc.paper.configuration; ++ ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.meta.NodeResolver; ++ ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++import java.lang.reflect.AnnotatedElement; ++ ++@Documented ++@Retention(RetentionPolicy.RUNTIME) ++@Target(ElementType.FIELD) ++public @interface NestedSetting { ++ ++ String[] value(); ++ ++ class Factory implements NodeResolver.Factory { ++ @Override ++ public @Nullable NodeResolver make(String name, AnnotatedElement element) { ++ if (element.isAnnotationPresent(NestedSetting.class)) { ++ Object[] path = element.getAnnotation(NestedSetting.class).value(); ++ if (path.length > 0) { ++ return node -> node.node(path); ++ } ++ } ++ return null; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b2e961bbd33c6ecb7f049365b7aff6c5caa262ff +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +@@ -0,0 +1,431 @@ ++package io.papermc.paper.configuration; ++ ++import com.google.common.base.Suppliers; ++import com.google.common.collect.Table; ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; ++import io.papermc.paper.configuration.serializer.ComponentSerializer; ++import io.papermc.paper.configuration.serializer.EnumValueSerializer; ++import io.papermc.paper.configuration.serializer.FastutilMapSerializer; ++import io.papermc.paper.configuration.serializer.PacketClassSerializer; ++import io.papermc.paper.configuration.serializer.StringRepresentableSerializer; ++import io.papermc.paper.configuration.serializer.TableSerializer; ++import io.papermc.paper.configuration.serializer.collections.MapSerializer; ++import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer; ++import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer; ++import io.papermc.paper.configuration.transformation.Transformations; ++import io.papermc.paper.configuration.transformation.global.LegacyPaperConfig; ++import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration; ++import io.papermc.paper.configuration.transformation.world.LegacyPaperWorldConfig; ++import io.papermc.paper.configuration.type.BooleanOrDefault; ++import io.papermc.paper.configuration.type.DoubleOrDefault; ++import io.papermc.paper.configuration.type.Duration; ++import io.papermc.paper.configuration.type.EngineMode; ++import io.papermc.paper.configuration.type.IntOrDefault; ++import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer; ++import it.unimi.dsi.fastutil.objects.Reference2IntMap; ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2LongMap; ++import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; ++import org.apache.commons.lang3.RandomStringUtils; ++import org.bukkit.command.Command; ++import org.bukkit.configuration.ConfigurationSection; ++import org.bukkit.configuration.file.YamlConfiguration; ++import org.jetbrains.annotations.VisibleForTesting; ++import org.slf4j.Logger; ++import org.spigotmc.SpigotConfig; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.BasicConfigurationNode; ++import org.spongepowered.configurate.ConfigurateException; ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.ConfigurationOptions; ++import org.spongepowered.configurate.NodePath; ++import org.spongepowered.configurate.objectmapping.ObjectMapper; ++import org.spongepowered.configurate.transformation.ConfigurationTransformation; ++import org.spongepowered.configurate.transformation.TransformAction; ++import org.spongepowered.configurate.yaml.YamlConfigurationLoader; ++ ++import java.io.File; ++import java.io.IOException; ++import java.lang.reflect.Type; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.function.Function; ++import java.util.function.Supplier; ++ ++import static com.google.common.base.Preconditions.checkState; ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++@SuppressWarnings("Convert2Diamond") ++public class PaperConfigurations extends Configurations { ++ ++ 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 WORLD_HEADER = map -> String.format(""" ++ This is a world configuration file for Paper. ++ This file may start empty but can be filled with settings to override ones in the %s/%s ++ ++ World: %s (%s)""", ++ PaperConfigurations.CONFIG_DIR, ++ PaperConfigurations.WORLD_DEFAULTS_CONFIG_FILE_NAME, ++ map.require(WORLD_NAME), ++ map.require(WORLD_KEY) ++ ); ++ ++ private static final String MOVED_NOTICE = """ ++ The global and world default configuration files have moved to %s ++ and the world-specific configuration file has been moved inside ++ the respective world folder. ++ ++ See https://docs.papermc.io/paper/configuration for more information. ++ """; ++ ++ private static final Supplier 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> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken>() {}, "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>() {}, new FastutilMapSerializer.SomethingToPrimitive>(Reference2IntOpenHashMap::new, Integer.TYPE)) ++ .register(new TypeToken>() {}, new FastutilMapSerializer.SomethingToPrimitive>(Reference2LongOpenHashMap::new, Long.TYPE)) ++ .register(new TypeToken>() {}, new TableSerializer()) ++ .register(new StringRepresentableSerializer()) ++ .register(IntOrDefault.SERIALIZER) ++ .register(DoubleOrDefault.SERIALIZER) ++ .register(BooleanOrDefault.SERIALIZER) ++ .register(Duration.SERIALIZER) ++ .register(EngineMode.SERIALIZER) ++ .register(FallbackValueSerializer.create(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), MinecraftServer::getServer)) ++ .register(new RegistryValueSerializer<>(new TypeToken>() {}, Registry.ENTITY_TYPE_REGISTRY, true)) ++ .register(new RegistryValueSerializer<>(Item.class, Registry.ITEM_REGISTRY, true)) ++ .register(new RegistryHolderSerializer<>(new TypeToken>() {}, 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 DEFAULT_AWARE_TRANSFORMATIONS = List.of(FeatureSeedsGeneration::apply); ++ ++ @Override ++ protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException { ++ final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder(); ++ // ADD FUTURE TRANSFORMS HERE (these transforms run after the defaults have been merged into the node) ++ DEFAULT_AWARE_TRANSFORMATIONS.forEach(transform -> transform.apply(builder, contextMap, defaultsNode)); ++ ++ ConfigurationTransformation transformation; ++ try { ++ transformation = builder.build(); // build throws IAE if no actions were provided (bad zml) ++ } catch (IllegalArgumentException ignored) { ++ return; ++ } ++ transformation.apply(worldNode); ++ } ++ ++ @Override ++ public WorldConfiguration createWorldConfig(final ContextMap contextMap) { ++ final String levelName = contextMap.require(WORLD_NAME); ++ try { ++ return super.createWorldConfig(contextMap); ++ } catch (IOException exception) { ++ throw new RuntimeException("Could not create world config for " + levelName, exception); ++ } ++ } ++ ++ @Override ++ protected boolean isConfigType(final Type type) { ++ return ConfigurationPart.class.isAssignableFrom(erase(type)); ++ } ++ ++ public void reloadConfigs(MinecraftServer server) { ++ try { ++ this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get())); ++ this.initializeWorldDefaultsConfiguration(); ++ for (ServerLevel level : server.getAllLevels()) { ++ this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig())); ++ } ++ } catch (Exception ex) { ++ throw new RuntimeException("Could not reload paper configuration files", ex); ++ } ++ } ++ ++ private static ContextMap createWorldContextMap(ServerLevel level) { ++ return createWorldContextMap(level.convertable.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.spigotConfig); ++ } ++ ++ public static ContextMap createWorldContextMap(Path dir, String levelName, ResourceLocation worldKey, SpigotWorldConfig spigotConfig) { ++ return ContextMap.builder() ++ .put(WORLD_DIRECTORY, dir) ++ .put(WORLD_NAME, levelName) ++ .put(WORLD_KEY, worldKey) ++ .put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, Suppliers.ofInstance(spigotConfig)) ++ .build(); ++ } ++ ++ public static PaperConfigurations setup(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception { ++ final Path legacy = Files.isSymbolicLink(legacyConfig) ? Files.readSymbolicLink(legacyConfig) : legacyConfig; ++ final Path replacementFile = legacy.resolveSibling(legacyConfig.getFileName() + "-README.txt"); ++ if (Files.notExists(replacementFile)) { ++ Files.createFile(replacementFile); ++ Files.writeString(replacementFile, String.format(MOVED_NOTICE, configDir.toAbsolutePath())); ++ } ++ if (needsConverting(legacyConfig)) { ++ try { ++ if (Files.exists(configDir) && !Files.isDirectory(configDir)) { ++ throw new RuntimeException("Paper needs to create a '" + CONFIG_DIR + "' folder in the root of your server. You already have a non-directory named '" + CONFIG_DIR + "'. Please remove it and restart the server."); ++ } ++ final Path backupDir = configDir.resolve(BACKUP_DIR); ++ if (Files.exists(backupDir) && !Files.isDirectory(backupDir)) { ++ throw new RuntimeException("Paper needs to create a '" + BACKUP_DIR + "' directory in the '" + CONFIG_DIR + "' folder. You already have a non-directory named '" + BACKUP_DIR + "'. Please remove it and restart the server."); ++ } ++ createDirectoriesSymlinkAware(backupDir); ++ final String backupFileName = legacyConfig.getFileName().toString() + ".old"; ++ final Path legacyConfigBackup = backupDir.resolve(backupFileName); ++ if (Files.exists(legacyConfigBackup) && !Files.isRegularFile(legacyConfigBackup)) { ++ throw new RuntimeException("Paper needs to create a '" + backupFileName + "' file in the '" + BACKUP_DIR + "' folder. You already have a non-file named '" + backupFileName + "'. Please remove it and restart the server."); ++ } ++ Files.move(legacyConfig.toRealPath(), legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup ++ if (Files.isSymbolicLink(legacyConfig)) { ++ Files.delete(legacyConfig); ++ } ++ convert(legacyConfigBackup, configDir, worldFolder, spigotConfig); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Could not convert '" + legacyConfig.getFileName().toString() + "' to the new configuration format", ex); ++ } ++ } ++ try { ++ createDirectoriesSymlinkAware(configDir); ++ return new PaperConfigurations(configDir); ++ } catch (final IOException ex) { ++ throw new RuntimeException("Could not setup PaperConfigurations", ex); ++ } ++ } ++ ++ private static void convert(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception { ++ createDirectoriesSymlinkAware(configDir); ++ ++ final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig); ++ final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME)); ++ final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME)); ++ ++ final ConfigurationNode legacy = legacyLoader.load(); ++ checkState(!legacy.virtual(), "can't be virtual"); ++ final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt(); ++ ++ final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy(); ++ checkState(!legacyWorldSettings.virtual(), "can't be virtual"); ++ legacy.removeChild("world-settings"); ++ ++ // Apply legacy transformations before settings flatten ++ final YamlConfiguration spigotConfiguration = loadLegacyConfigFile(spigotConfig); // needs to change spigot config values in this transformation ++ LegacyPaperConfig.transformation(spigotConfiguration).apply(legacy); ++ spigotConfiguration.save(spigotConfig); ++ legacy.mergeFrom(legacy.node("settings")); // flatten "settings" to root ++ legacy.removeChild("settings"); ++ LegacyPaperConfig.toNewFormat().apply(legacy); ++ globalLoader.save(legacy); // save converted node to new global location ++ ++ final ConfigurationNode worldDefaults = legacyWorldSettings.node("default").copy(); ++ checkState(!worldDefaults.virtual()); ++ worldDefaults.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version); ++ legacyWorldSettings.removeChild("default"); ++ LegacyPaperWorldConfig.transformation().apply(worldDefaults); ++ LegacyPaperWorldConfig.toNewFormat().apply(worldDefaults); ++ worldDefaultsLoader.save(worldDefaults); ++ ++ legacyWorldSettings.childrenMap().forEach((world, legacyWorldNode) -> { ++ try { ++ legacyWorldNode.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version); ++ LegacyPaperWorldConfig.transformation().apply(legacyWorldNode); ++ LegacyPaperWorldConfig.toNewFormat().apply(legacyWorldNode); ++ ConfigurationLoaders.naturallySortedWithoutHeader(worldFolder.resolve(world.toString()).resolve(WORLD_CONFIG_FILE_NAME)).save(legacyWorldNode); // save converted node to new location ++ } catch (final ConfigurateException ex) { ++ ex.printStackTrace(); ++ } ++ }); ++ } ++ ++ private static boolean needsConverting(final Path legacyConfig) { ++ return Files.exists(legacyConfig) && Files.isRegularFile(legacyConfig); ++ } ++ ++ @Deprecated ++ public YamlConfiguration createLegacyObject(final MinecraftServer server) { ++ YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile()); ++ ConfigurationSection worlds = global.createSection("__________WORLDS__________"); ++ worlds.set("__defaults__", YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile())); ++ for (ServerLevel level : server.getAllLevels()) { ++ worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile())); ++ } ++ return global; ++ } ++ ++ @Deprecated ++ public static YamlConfiguration loadLegacyConfigFile(File configFile) throws Exception { ++ YamlConfiguration config = new YamlConfiguration(); ++ if (configFile.exists()) { ++ try { ++ config.load(configFile); ++ } catch (Exception ex) { ++ throw new Exception("Failed to load configuration file: " + configFile.getName(), ex); ++ } ++ } ++ return config; ++ } ++ ++ @VisibleForTesting ++ static ConfigurationNode createForTesting() { ++ ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build(); ++ ConfigurationOptions options = defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults())) ++ .serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer())); ++ return BasicConfigurationNode.root(options); ++ } ++ ++ // Symlinks are not correctly checked in createDirectories ++ static void createDirectoriesSymlinkAware(Path path) throws IOException { ++ if (!Files.isDirectory(path)) { ++ Files.createDirectories(path); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1bb16fc7598cd53e822d84b69d6a9727b37f484f +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java +@@ -0,0 +1,63 @@ ++package io.papermc.paper.configuration; ++ ++import org.spongepowered.configurate.NodePath; ++ ++import static org.spongepowered.configurate.NodePath.path; ++ ++interface RemovedConfigurations { ++ ++ NodePath[] REMOVED_WORLD_PATHS = { ++ path("elytra-hit-wall-damage"), ++ path("queue-light-updates"), ++ path("save-queue-limit-for-auto-save"), ++ path("max-chunk-sends-per-tick"), ++ path("max-chunk-gens-per-tick"), ++ path("fire-physics-event-for-redstone"), ++ path("fix-zero-tick-instant-grow-farms"), ++ path("bed-search-radius"), ++ path("lightning-strike-distance-limit"), ++ path("fix-wither-targeting-bug"), ++ path("remove-corrupt-tile-entities"), ++ path("allow-undead-horse-leashing"), ++ path("reset-arrow-despawn-timer-on-fall"), ++ path("seed-based-feature-search"), ++ path("seed-based-feature-search-loads-chunks"), ++ path("viewdistances.no-tick-view-distance"), ++ path("seed-based-feature-search"), // unneeded as of 1.18 ++ path("seed-based-feature-search-loads-chunks"), // unneeded as of 1.18 ++ path("reset-arrow-despawn-timer-on-fall"), ++ path("squid-spawn-height"), ++ path("viewdistances"), ++ path("use-alternate-fallingblock-onGround-detection"), ++ path("skip-entity-ticking-in-chunks-scheduled-for-unload"), ++ path("tracker-update-distance"), ++ path("allow-block-location-tab-completion"), ++ path("cache-chunk-maps"), ++ path("disable-mood-sounds"), ++ path("fix-cannons"), ++ path("player-blocking-damage-multiplier"), ++ path("remove-invalid-mob-spawner-tile-entities"), ++ path("use-hopper-check"), ++ path("use-async-lighting"), ++ path("tnt-explosion-volume"), ++ path("entities", "spawning", "despawn-ranges", "soft"), ++ path("entities", "spawning", "despawn-ranges", "hard") ++ }; ++ ++ NodePath[] REMOVED_GLOBAL_PATHS = { ++ path("queue-light-updates-max-loss"), ++ path("sleep-between-chunk-saves"), ++ path("remove-invalid-statistics"), ++ path("min-chunk-load-threads"), ++ path("use-versioned-world"), ++ path("save-player-data"), // to spigot (converted) ++ path("log-named-entity-deaths"), // default in vanilla ++ path("chunk-tasks-per-tick"), // removed in tuinity merge ++ path("item-validation", "loc-name"), ++ path("commandErrorMessage"), ++ path("baby-zombie-movement-speed"), ++ path("limit-player-interactions"), ++ path("warnWhenSettingExcessiveVelocity") ++ }; ++ ++} +diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e2c612dd55fcb2769fb06f7878b8d0873f2be139 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +@@ -0,0 +1,467 @@ ++package io.papermc.paper.configuration; ++ ++import com.google.common.collect.HashBasedTable; ++import com.google.common.collect.Table; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.constraint.Constraint; ++import io.papermc.paper.configuration.constraint.Constraints; ++import io.papermc.paper.configuration.legacy.MaxEntityCollisionsInitializer; ++import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; ++import io.papermc.paper.configuration.legacy.SpawnLoadedRangeInitializer; ++import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration; ++import io.papermc.paper.configuration.type.BooleanOrDefault; ++import io.papermc.paper.configuration.type.DoubleOrDefault; ++import io.papermc.paper.configuration.type.Duration; ++import io.papermc.paper.configuration.type.EngineMode; ++import io.papermc.paper.configuration.type.IntOrDefault; ++import io.papermc.paper.configuration.type.fallback.ArrowDespawnRate; ++import io.papermc.paper.configuration.type.fallback.AutosavePeriod; ++import it.unimi.dsi.fastutil.objects.Reference2IntMap; ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2LongMap; ++import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; ++import net.minecraft.Util; ++import net.minecraft.core.Holder; ++import net.minecraft.core.Registry; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.world.Difficulty; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.MobCategory; ++import net.minecraft.world.entity.monster.Vindicator; ++import net.minecraft.world.entity.monster.Zombie; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.level.NaturalSpawner; ++import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; ++import org.slf4j.Logger; ++import org.spigotmc.SpigotWorldConfig; ++import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++import org.spongepowered.configurate.objectmapping.meta.Required; ++import org.spongepowered.configurate.objectmapping.meta.Setting; ++ ++import java.util.Arrays; ++import java.util.List; ++import java.util.Map; ++import java.util.function.Function; ++import java.util.stream.Collectors; ++ ++@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) ++public class WorldConfiguration extends ConfigurationPart { ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ static final int CURRENT_VERSION = 28; ++ ++ private transient final SpigotWorldConfig spigotConfig; ++ private transient final ResourceLocation worldKey; ++ WorldConfiguration(SpigotWorldConfig spigotConfig, ResourceLocation worldKey) { ++ this.spigotConfig = spigotConfig; ++ this.worldKey = worldKey; ++ } ++ ++ public boolean isDefault() { ++ return this.worldKey.equals(PaperConfigurations.WORLD_DEFAULTS_KEY); ++ } ++ ++ @Setting(Configuration.VERSION_FIELD) ++ public int version = CURRENT_VERSION; ++ ++ public Anticheat anticheat; ++ ++ public class Anticheat extends ConfigurationPart { ++ ++ public Obfuscation obfuscation; ++ ++ public class Obfuscation extends ConfigurationPart { ++ public Items items = new Items(); ++ public class Items extends ConfigurationPart { ++ public boolean hideItemmeta = false; ++ public boolean hideDurability = false; ++ } ++ } ++ ++ public AntiXray antiXray; ++ ++ public class AntiXray extends ConfigurationPart { ++ public boolean enabled = false; ++ public EngineMode engineMode = EngineMode.HIDE; ++ public int maxBlockHeight = 64; ++ public int updateRadius = 2; ++ public boolean lavaObscures = false; ++ public boolean usePermission = false; ++ public List 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 ++ public List replacementBlocks = List.of("stone", "oak_planks", "deepslate"); // TODO update type to List ++ } ++ } ++ ++ 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 spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1))); ++ @MergeMap ++ public Map 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 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 zombie = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList(); ++ public List husk = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList(); ++ @Setting("zombie_villager") ++ public List zombieVillager = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList(); ++ @Setting("zombified_piglin") ++ public List zombified_piglin = Arrays.stream(Difficulty.values()).filter(Zombie.DOOR_BREAKING_PREDICATE).toList(); ++ public List vindicator = Arrays.stream(Difficulty.values()).filter(Vindicator.DOOR_BREAKING_PREDICATE).toList(); ++ ++ // TODO remove when this becomes a proper map ++ public List get(EntityType type) { ++ return this.getOrDefault(type, null); ++ } ++ ++ public List getOrDefault(EntityType type, List 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> 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, String, Integer> sensor = Util.make(HashBasedTable.create(), table -> table.put(EntityType.VILLAGER, "secondarypoisensor", 40)); ++ public Table, 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>> 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> value(); ++ ++ class Factory implements org.spongepowered.configurate.objectmapping.meta.Constraint.Factory { ++ @SuppressWarnings("unchecked") ++ @Override ++ public org.spongepowered.configurate.objectmapping.meta.Constraint make(final Constraint data, final Type type) { ++ try { ++ final Constructor> constructor = data.value().getDeclaredConstructor(); ++ constructor.trySetAccessible(); ++ return (org.spongepowered.configurate.objectmapping.meta.Constraint) 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 { ++ ++ 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 { ++ @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 { ++ @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 { ++ @Override ++ public Constraint 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 value(); ++ ++ final class Factory implements NodeResolver.Factory { ++ ++ private final SpigotWorldConfig spigotWorldConfig; ++ private final Table, 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 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 { ++ ++ 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> typeSupported) { ++ return MiniMessage.miniMessage().serialize(component); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..27c0679d376bb31ab52131dfea74b3b580ca92b5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/EngineModeSerializer.java +@@ -0,0 +1,33 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import io.papermc.paper.configuration.type.EngineMode; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.function.Predicate; ++ ++public final class EngineModeSerializer extends ScalarSerializer { ++ ++ public EngineModeSerializer() { ++ super(EngineMode.class); ++ } ++ ++ @Override ++ public EngineMode deserialize(Type type, Object obj) throws SerializationException { ++ if (obj instanceof Integer id) { ++ try { ++ return EngineMode.valueOf(id); ++ } catch (IllegalArgumentException e) { ++ throw new SerializationException(id + " is not a valid id for type " + type + " for this node"); ++ } ++ } ++ ++ throw new SerializationException(obj + " is not of a valid type " + type + " for this node"); ++ } ++ ++ @Override ++ protected Object serialize(EngineMode item, Predicate> typeSupported) { ++ return item.getId(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2afb9268447792e3cdb46172b2050dbce066c59a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/serializer/EnumValueSerializer.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.configuration.serializer; ++ ++import com.mojang.logging.LogUtils; ++import io.leangen.geantyref.TypeToken; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.util.EnumLookup; ++ ++import java.lang.reflect.Type; ++import java.util.Arrays; ++import java.util.List; ++import java.util.function.Predicate; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++/** ++ * Enum serializer that lists options if fails and accepts `-` as `_`. ++ */ ++public class EnumValueSerializer extends ScalarSerializer> { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public EnumValueSerializer() { ++ super(new TypeToken>() {}); ++ } ++ ++ @SuppressWarnings({"rawtypes", "unchecked"}) ++ @Override ++ public @Nullable Enum deserialize(final Type type, final Object obj) throws SerializationException { ++ final String enumConstant = obj.toString(); ++ final Class 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 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> 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> implements TypeSerializer { ++ private final Function factory; ++ ++ protected FastutilMapSerializer(final Function 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> extends FastutilMapSerializer { ++ private final Type primitiveType; ++ ++ public SomethingToPrimitive(final Function 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> extends FastutilMapSerializer { ++ private final Type primitiveType; ++ ++ public PrimitiveToSomething(final Function 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>> implements MapSerializer.WriteBack { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final TypeToken>> TYPE = new TypeToken>>() {}; ++ private static final List SUBPACKAGES = List.of("game", "handshake", "login", "status"); ++ private static final BiMap MOJANG_TO_OBF; ++ ++ static { ++ final ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); ++ final @Nullable Map 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> 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>) packetClass; ++ } ++ ++ @Override ++ protected @Nullable Object serialize(final Class> packetClass, final Predicate> 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 { ++ private static final Map> 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 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> 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> { ++ 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 void deserialize0(final Table 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 = (TypeSerializer) 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> columnValueSerializer = (TypeSerializer>) 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 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 void serialize0(final Table 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> { ++ ++ public static final TypeToken> TYPE = new TypeToken>() {}; ++ ++ 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 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 keysToClear = new HashSet<>(); ++ for (Map.Entry 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 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 extends ScalarSerializer { ++ ++ private final ResourceKey> registryKey; ++ private final boolean omitMinecraftNamespace; ++ ++ protected RegistryEntrySerializer(TypeToken type, ResourceKey> registryKey, boolean omitMinecraftNamespace) { ++ super(type); ++ this.registryKey = registryKey; ++ this.omitMinecraftNamespace = omitMinecraftNamespace; ++ } ++ ++ protected RegistryEntrySerializer(Class type, ResourceKey> registryKey, boolean omitMinecraftNamespace) { ++ super(type); ++ this.registryKey = registryKey; ++ this.omitMinecraftNamespace = omitMinecraftNamespace; ++ } ++ ++ protected final Registry registry() { ++ return MinecraftServer.getServer().registryAccess().registryOrThrow(this.registryKey); ++ } ++ ++ protected abstract T convertFromResourceKey(ResourceKey key) throws SerializationException; ++ ++ @Override ++ public final T deserialize(Type type, Object obj) throws SerializationException { ++ return this.convertFromResourceKey(this.deserializeKey(obj)); ++ } ++ ++ protected abstract ResourceKey convertToResourceKey(T value); ++ ++ @Override ++ protected final Object serialize(T item, Predicate> typeSupported) { ++ final ResourceKey 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 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 extends RegistryEntrySerializer, T> { ++ ++ @SuppressWarnings("unchecked") ++ public RegistryHolderSerializer(TypeToken typeToken, ResourceKey> registryKey, boolean omitMinecraftNamespace) { ++ super((TypeToken>) TypeToken.get(TypeFactory.parameterizedClass(Holder.class, typeToken.getType())), registryKey, omitMinecraftNamespace); ++ } ++ ++ public RegistryHolderSerializer(Class type, ResourceKey> 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 convertFromResourceKey(ResourceKey key) throws SerializationException { ++ return this.registry().getHolder(key).orElseThrow(() -> new SerializationException("Missing holder in " + this.registry().key() + " with key " + key)); ++ } ++ ++ @Override ++ protected ResourceKey convertToResourceKey(Holder 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 extends RegistryEntrySerializer { ++ ++ public RegistryValueSerializer(TypeToken type, ResourceKey> registryKey, boolean omitMinecraftNamespace) { ++ super(type, registryKey, omitMinecraftNamespace); ++ } ++ ++ public RegistryValueSerializer(Class type, ResourceKey> registryKey, boolean omitMinecraftNamespace) { ++ super(type, registryKey, omitMinecraftNamespace); ++ } ++ ++ @Override ++ protected T convertFromResourceKey(ResourceKey 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 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 englishCheck, final String i18nKey, final String... strPath) { ++ miniMessageWithTranslatable(builder, englishCheck, Component.translatable(i18nKey), strPath); ++ } ++ private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate 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>> features = Objects.requireNonNullElseGet(featureNode.get(new TypeToken>>>() {}), 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>>>() {}, 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 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 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> 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 rebuild = new HashMap<>(); ++ value.childrenMap().forEach((key, node) -> { ++ String itemName = key.toString(); ++ final Optional> 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 SERIALIZER = new Serializer(); ++ ++ public boolean or(boolean fallback) { ++ return this.value != null && this.value; ++ } ++ ++ private static final class Serializer extends ScalarSerializer { ++ 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> 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 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 { ++ 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> 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 { ++ 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> typeSupported) { ++ return item.value(); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/EngineMode.java b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java +new file mode 100644 +index 0000000000000000000000000000000000000000..99e90636051fa0c770ee2eafb7f0d29c8195f9ae +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/EngineMode.java +@@ -0,0 +1,37 @@ ++package io.papermc.paper.configuration.type; ++ ++import io.papermc.paper.configuration.serializer.EngineModeSerializer; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++ ++public enum EngineMode { ++ ++ HIDE(1, "hide ores"), OBFUSCATE(2, "obfuscate"); ++ ++ public static final ScalarSerializer SERIALIZER = new EngineModeSerializer(); ++ ++ private final int id; ++ private final String description; ++ ++ EngineMode(int id, String description) { ++ this.id = id; ++ this.description = description; ++ } ++ ++ public static EngineMode valueOf(int id) { ++ for (EngineMode engineMode : values()) { ++ if (engineMode.getId() == id) { ++ return engineMode; ++ } ++ } ++ ++ throw new IllegalArgumentException("No enum constant with id " + id); ++ } ++ ++ public int getId() { ++ return id; ++ } ++ ++ public String getDescription() { ++ return description; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3278045dbf081cc4099e2eac3a6c4fac3012be4b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java +@@ -0,0 +1,56 @@ ++package io.papermc.paper.configuration.type; ++ ++import com.mojang.logging.LogUtils; ++import org.apache.commons.lang3.math.NumberUtils; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import java.lang.reflect.Type; ++import java.util.OptionalInt; ++import java.util.function.Predicate; ++ ++public record IntOrDefault(OptionalInt value) { ++ private static final String DEFAULT_VALUE = "default"; ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ public static final IntOrDefault USE_DEFAULT = new IntOrDefault(OptionalInt.empty()); ++ public static final ScalarSerializer SERIALIZER = new Serializer(); ++ ++ public int or(final int fallback) { ++ return this.value.orElse(fallback); ++ } ++ ++ private static final class Serializer extends ScalarSerializer { ++ 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> 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, Object> context, Object value) throws SerializationException { ++ super(context, fromObject(value)); ++ } ++ ++ private ArrowDespawnRate(Map, Object> context) { ++ super(context, OptionalInt.empty()); ++ } ++ ++ @Override ++ protected OptionalInt process(int value) { ++ return Util.negToDef(value); ++ } ++ ++ @Override ++ public Set> 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, Object> contextMap, Object value) throws SerializationException { ++ super(contextMap, fromObject(value)); ++ } ++ ++ private AutosavePeriod(Map, Object> contextMap) { ++ super(contextMap, OptionalInt.empty()); ++ } ++ ++ @Override ++ protected OptionalInt process(int value) { ++ return Util.negToDef(value); ++ } ++ ++ @Override ++ protected Set> 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 SPIGOT_WORLD_CONFIG = new ContextKey<>("SpigotWorldConfig"); ++ static final ContextKey> MINECRAFT_SERVER = new ContextKey<>("MinecraftServer"); ++ ++ private final Map, Object> contextMap; ++ ++ protected FallbackValue(Map, 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> required(); ++ ++ @SuppressWarnings("unchecked") ++ protected T get(ContextKey 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, 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 { ++ ++ private final String name; ++ ++ ContextKey(String name) { ++ this.name = name; ++ } ++ ++ @Override ++ public String toString() { ++ return this.name; ++ } ++ ++ Map, 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 { ++ ++ private static final Map, FallbackCreator> REGISTRY = new HashMap<>(); ++ ++ static { ++ REGISTRY.put(ArrowDespawnRate.class, ArrowDespawnRate::new); ++ REGISTRY.put(AutosavePeriod.class, AutosavePeriod::new); ++ } ++ ++ FallbackValueSerializer(Map, Object> contextMap) { ++ super(FallbackValue.class); ++ this.contextMap = contextMap; ++ } ++ ++ @FunctionalInterface ++ private interface FallbackCreator { ++ T create(Map, Object> context, Object value) throws SerializationException; ++ } ++ ++ private final Map, 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> typeSupported) { ++ return item.serialize(); ++ } ++ ++ public static FallbackValueSerializer create(SpigotWorldConfig config, Supplier 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 15ecb1769a0604eed348b0cd31b86ce2010cbda0..8d65c989aef5ec92873a504f5b331dfe7d8b4bff 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -281,6 +281,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); +@@ -371,6 +372,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List 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 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 getTypeKey(); + +- protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, Holder holder, Supplier 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 resourcekey, Holder holder, Supplier 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 paperWorldConfigCreator) { // Paper + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot ++ this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper + this.generator = gen; + this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env); + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index dbaec6fc4967d8140fd5af68456894bad1a0205a..4f67f7541ff257f35701610760fb8e04e8379fe3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -870,6 +870,7 @@ public final class CraftServer implements Server { + } + + org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot ++ this.console.paperConfigurations.reloadConfigs(this.console); + for (ServerLevel world : this.console.getAllLevels()) { + world.serverLevelData.setDifficulty(config.difficulty); + world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals); +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index eca7833e722a876be29806c92b18b6b58aab5725..e8d71985f2e96574081e4f609d62a3b8bded8249 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 List getList(String path, T def) +diff --git a/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0396589795da1f83ddf62426236dde9a3afa1376 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java +@@ -0,0 +1,20 @@ ++package io.papermc.paper.configuration; ++ ++import org.spongepowered.configurate.ConfigurationNode; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public final class GlobalConfigTestingBase { ++ ++ public static void setupGlobalConfigForTest() { ++ //noinspection ConstantConditions ++ if (GlobalConfiguration.get() == null) { ++ ConfigurationNode node = PaperConfigurations.createForTesting(); ++ try { ++ GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class); ++ GlobalConfiguration.set(globalConfiguration); ++ } catch (SerializationException e) { ++ throw new RuntimeException(e); ++ } ++ } ++ } ++} +diff --git a/src/test/java/org/bukkit/support/AbstractTestingBase.java b/src/test/java/org/bukkit/support/AbstractTestingBase.java +index 6816d8a9fa504ca5a25fa62c0f0974e3e744ead6..e73a9a957cd55bf838e301ed531295162f2cfb89 100644 +--- a/src/test/java/org/bukkit/support/AbstractTestingBase.java ++++ b/src/test/java/org/bukkit/support/AbstractTestingBase.java +@@ -46,6 +46,7 @@ public abstract class AbstractTestingBase { + + DummyServer.setup(); + DummyEnchantments.setup(); ++ io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(); // Paper + + ImmutableList.Builder builder = ImmutableList.builder(); + for (Material m : Material.values()) { diff --git a/patches/server/1025-Use-MOTD-provided-in-Paper-config-if-it-exists.patch b/patches/server/1025-Use-MOTD-provided-in-Paper-config-if-it-exists.patch new file mode 100644 index 000000000..1a4628572 --- /dev/null +++ b/patches/server/1025-Use-MOTD-provided-in-Paper-config-if-it-exists.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: wordandahalf <10679776+wordandahalf@users.noreply.github.com> +Date: Sun, 7 Aug 2022 20:06:23 -0500 +Subject: [PATCH] Use MOTD provided in Paper config, if it exists + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index bfde5bbcccfaa754ec6bdf4f3817981a93e465bd..750ed134ebf520ce91f9ea3d4ead3a56b624b566 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -2184,6 +2184,12 @@ public final class CraftServer implements Server { + // Paper start + @Override + public net.kyori.adventure.text.Component motd() { ++ net.kyori.adventure.text.Component paperMotd = ++ io.papermc.paper.configuration.GlobalConfiguration.get().messages.motd; ++ ++ if (paperMotd != null) ++ return paperMotd; ++ + return console.getComponentMotd(); + } + // Paper end