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 35839e1c2dede1e3a1e54b124667cf2bf63e73f0..a87cf2a4ad955b7429269f2d46adbe59f6e4525e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation("org.apache.logging.log4j:log4j-iostreams:2.17.1") // Paper implementation("org.ow2.asm:asm:9.3") implementation("org.ow2.asm:asm-commons:9.3") // Paper - ASM event executor generation + implementation("org.spongepowered:configurate-yaml:4.1.2") // Paper - config files implementation("commons-lang:commons-lang:2.6") runtimeOnly("org.xerial:sqlite-jdbc:3.36.0.3") runtimeOnly("mysql:mysql-connector-java:8.0.29") diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..ef41cf3a7d1e6f2bfe81e0fb865d2f969bbc77c1 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -0,0 +1,8 @@ +package com.destroystokyo.paper; + +/** + * @deprecated kept as a means to identify Paper in older plugins/PaperLib + */ +@Deprecated(forRemoval = true) +public class PaperConfig { +} diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..c91f109b4cf64dc1b4ef09f38e1cb8bf5cb2be13 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java @@ -0,0 +1,8 @@ +package com.destroystokyo.paper; + +/** + * @deprecated kept as a means to identify Paper in older plugins/PaperLib + */ +@Deprecated(forRemoval = true) +public class PaperWorldConfig { +} diff --git a/src/main/java/io/papermc/paper/configuration/Configuration.java b/src/main/java/io/papermc/paper/configuration/Configuration.java new file mode 100644 index 0000000000000000000000000000000000000000..817fd26cc3591f9cae0f61f4036dde43c4ed60e8 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/Configuration.java @@ -0,0 +1,13 @@ +package io.papermc.paper.configuration; + +public final class Configuration { + public static final String VERSION_FIELD = "_version"; + @Deprecated + public static final String LEGACY_CONFIG_VERSION_FIELD = "config-version"; + + @Deprecated + public static final int FINAL_LEGACY_VERSION = 27; + + private Configuration() { + } +} diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java new file mode 100644 index 0000000000000000000000000000000000000000..cb7d11dcf13c6ac464634a7e8115bf3dee0e72a2 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/ConfigurationLoaders.java @@ -0,0 +1,26 @@ +package io.papermc.paper.configuration; + +import java.nio.file.Path; +import org.spongepowered.configurate.loader.HeaderMode; +import org.spongepowered.configurate.util.MapFactories; +import org.spongepowered.configurate.yaml.NodeStyle; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +public final class ConfigurationLoaders { + private ConfigurationLoaders() { + } + + public static YamlConfigurationLoader.Builder naturallySorted() { + return YamlConfigurationLoader.builder() + .indent(2) + .nodeStyle(NodeStyle.BLOCK) + .defaultOptions(options -> options.mapFactory(MapFactories.sortedNatural())); + } + + public static YamlConfigurationLoader naturallySortedWithoutHeader(final Path path) { + return naturallySorted() + .headerMode(HeaderMode.NONE) + .path(path) + .build(); + } +} diff --git a/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java new file mode 100644 index 0000000000000000000000000000000000000000..7a4a7a654fe2516ed894a68f2657344df9d70f4c --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/ConfigurationPart.java @@ -0,0 +1,10 @@ +package io.papermc.paper.configuration; + +abstract class ConfigurationPart { + + public static abstract class Post extends ConfigurationPart { + + public abstract void postProcess(); + } + +} diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java new file mode 100644 index 0000000000000000000000000000000000000000..13d7d1c24ec9192d0163f6eedeac8fca82b6e80c --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/Configurations.java @@ -0,0 +1,296 @@ +package io.papermc.paper.configuration; + +import io.leangen.geantyref.TypeToken; +import io.papermc.paper.configuration.constraint.Constraint; +import io.papermc.paper.configuration.constraint.Constraints; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.MustBeInvokedByOverriders; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.util.CheckedFunction; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.UnaryOperator; + +public abstract class Configurations { + + 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)) { + Files.createDirectories(dir); + Files.createFile(worldConfigFile); // create empty file as template + newFile = true; + } + + final YamlConfigurationLoader worldLoader = this.createWorldConfigLoaderBuilder(contextMap) + .defaultOptions(this.applyObjectMapperFactory(this.createWorldObjectMapperFactoryBuilder(contextMap).build())) + .path(worldConfigFile) + .build(); + final ConfigurationNode worldNode = worldLoader.load(); + if (newFile) { // set the version field if new file + worldNode.node(Configuration.VERSION_FIELD).set(WorldConfiguration.CURRENT_VERSION); + } + this.applyWorldConfigTransformations(contextMap, worldNode); + this.applyDefaultsAwareWorldConfigTransformations(contextMap, worldNode, defaultsNode); + worldLoader.save(worldNode); // save before loading node NOTE: don't save the backing node after loading it, or you'll fill up the world-specific config + worldNode.mergeFrom(defaultsNode); + return creator.apply(worldNode); + } + + protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException { + } + + protected void applyDefaultsAwareWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode worldNode, final ConfigurationNode defaultsNode) throws ConfigurateException { + } + + private UnaryOperator 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..5a808a09291da691cbee75a55f6aa1b70ac9f018 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -0,0 +1,264 @@ +package io.papermc.paper.configuration; + +import co.aikar.timings.MinecraftTimings; +import com.destroystokyo.paper.io.chunk.ChunkTaskManager; +import io.papermc.paper.configuration.constraint.Constraint; +import io.papermc.paper.configuration.constraint.Constraints; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; +import org.spongepowered.configurate.objectmapping.meta.Required; +import org.spongepowered.configurate.objectmapping.meta.Setting; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) +public class GlobalConfiguration extends ConfigurationPart { + static final int CURRENT_VERSION = 28; + private static GlobalConfiguration instance; + public static GlobalConfiguration get() { + return instance; + } + static void set(GlobalConfiguration instance) { + GlobalConfiguration.instance = instance; + } + + @Setting(Configuration.VERSION_FIELD) + public int version = CURRENT_VERSION; + + public Messages messages; + + public class Messages extends ConfigurationPart { + public Kick kick; + + public class Kick extends ConfigurationPart { + public Component authenticationServersDown = Component.translatable("multiplayer.disconnect.authservers_down"); + public Component connectionThrottle = Component.text("Connection throttled! Please wait before reconnecting."); + public Component flyingPlayer = Component.translatable("multiplayer.disconnect.flying"); + public Component flyingVehicle = Component.translatable("multiplayer.disconnect.flying"); + } + + public Component noPermission = Component.text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", NamedTextColor.RED); + public boolean useDisplayNameInQuitMessage = false; + } + + public Timings timings; + + public class Timings extends ConfigurationPart.Post { + public boolean enabled = true; + public boolean verbose = true; + public String url = "https://timings.aikar.co/"; + public boolean serverNamePrivacy = false; + public List 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(@Constraint(Constraints.Positive.class) @Required double interval, @Constraint(Constraints.Positive.class) @Required double maxPacketRate, ViolateAction action) { + public PacketLimit(final double interval, final double maxPacketRate, final @Nullable ViolateAction action) { + this.interval = interval; + this.maxPacketRate = maxPacketRate; + this.action = Objects.requireNonNullElse(action, ViolateAction.KICK); + } + + public boolean isEnabled() { + return this.interval > 0.0 && this.maxPacketRate > 0.0; + } + + public enum ViolateAction { + KICK, + DROP; + } + } + } + + public Collisions collisions; + + public class Collisions extends ConfigurationPart { + public boolean enablePlayerCollisions = true; + public boolean sendFullPosForHardCollidingEntities = true; + } + + public PlayerAutoSave playerAutoSave; + + + public class PlayerAutoSave extends ConfigurationPart { + public int rate = -1; + private int maxPerTick = -1; + public int maxPerTick() { + if (this.maxPerTick < 0) { + return (this.rate == 1 || this.rate > 100) ? 10 : 20; + } + return this.maxPerTick; + } + } + + public Misc misc; + + public class Misc extends ConfigurationPart { + public int maxJoinsPerTick = 3; + public boolean fixEntityPositionDesync = true; + public boolean loadPermissionsYmlBeforePlugins = true; + @Constraints.Min(4) + public int regionFileCacheSize = 256; + @Comment("See https://luckformula.emc.gs") + public boolean useAlternativeLuckFormula = false; + public boolean lagCompensateBlockBreaking = true; + public boolean useDimensionTypeForCustomSpawners = false; + } +} diff --git a/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java new file mode 100644 index 0000000000000000000000000000000000000000..b33c7e67cff07c801d1b7aa2bc342ab5a9c1bad3 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java @@ -0,0 +1,118 @@ +package io.papermc.paper.configuration; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spigotmc.SpigotWorldConfig; +import org.spongepowered.configurate.objectmapping.FieldDiscoverer; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.util.CheckedSupplier; + +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static io.leangen.geantyref.GenericTypeReflector.erase; + +final class InnerClassFieldDiscoverer implements FieldDiscoverer> { + + 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(); + } + + @Override + public void complete(Object instance, Map intermediate) throws SerializationException { + mutableInstanceFactoryDelegate.complete(instance, intermediate); + } + + @Override + public Object complete(Map intermediate) throws SerializationException { + Object value = mutableInstanceFactoryDelegate.complete(intermediate); + if (value instanceof ConfigurationPart.Post post) { + post.postProcess(); + } + return value; + } + + @Override + public boolean canCreateInstances() { + return mutableInstanceFactoryDelegate.canCreateInstances(); + } + }; + } + } + return null; + } + + private Map, 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/NestedSetting.java b/src/main/java/io/papermc/paper/configuration/NestedSetting.java new file mode 100644 index 0000000000000000000000000000000000000000..69add4a7f1147015806bc9b63a8340d1893356c1 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/NestedSetting.java @@ -0,0 +1,32 @@ +package io.papermc.paper.configuration; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.objectmapping.meta.NodeResolver; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface NestedSetting { + + String[] value(); + + class Factory implements NodeResolver.Factory { + @Override + public @Nullable NodeResolver make(String name, AnnotatedElement element) { + if (element.isAnnotationPresent(NestedSetting.class)) { + Object[] path = element.getAnnotation(NestedSetting.class).value(); + if (path.length > 0) { + return node -> node.node(path); + } + } + return null; + } + } +} diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java new file mode 100644 index 0000000000000000000000000000000000000000..be64437ba7630fe069aaea502932b514c85162f8 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java @@ -0,0 +1,384 @@ +package io.papermc.paper.configuration; + +import com.google.common.base.Suppliers; +import com.google.common.collect.Table; +import com.mojang.logging.LogUtils; +import io.leangen.geantyref.TypeToken; +import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; +import io.papermc.paper.configuration.serializer.ComponentSerializer; +import io.papermc.paper.configuration.serializer.EnumValueSerializer; +import io.papermc.paper.configuration.serializer.FastutilMapSerializer; +import io.papermc.paper.configuration.serializer.PacketClassSerializer; +import io.papermc.paper.configuration.serializer.StringRepresentableSerializer; +import io.papermc.paper.configuration.serializer.TableSerializer; +import io.papermc.paper.configuration.serializer.collections.MapSerializer; +import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer; +import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer; +import io.papermc.paper.configuration.transformation.Transformations; +import io.papermc.paper.configuration.transformation.global.LegacyPaperConfig; +import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration; +import io.papermc.paper.configuration.transformation.world.LegacyPaperWorldConfig; +import io.papermc.paper.configuration.type.BooleanOrDefault; +import io.papermc.paper.configuration.type.DoubleOrDefault; +import io.papermc.paper.configuration.type.Duration; +import io.papermc.paper.configuration.type.IntOrDefault; +import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2LongMap; +import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import org.apache.commons.lang3.RandomStringUtils; +import org.bukkit.command.Command; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.spigotmc.SpigotConfig; +import org.spigotmc.SpigotWorldConfig; +import org.spongepowered.configurate.BasicConfigurationNode; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; +import org.spongepowered.configurate.NodePath; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.transformation.ConfigurationTransformation; +import org.spongepowered.configurate.transformation.TransformAction; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkState; +import static io.leangen.geantyref.GenericTypeReflector.erase; + +@SuppressWarnings("Convert2Diamond") +public class PaperConfigurations extends Configurations { + + private static final Logger LOGGER = LogUtils.getLogger(); + static final String GLOBAL_CONFIG_FILE_NAME = "paper-global.yml"; + static final String WORLD_DEFAULTS_CONFIG_FILE_NAME = "paper-world-defaults.yml"; + static final String WORLD_CONFIG_FILE_NAME = "paper-world.yml"; + private static final Path BACKUP_DIR = Path.of("legacy-backup"); + + private static final String GLOBAL_HEADER = """ + This is the global configuration file for Paper. + As you can see, there's a lot to configure. Some options may impact gameplay, so use + with caution, and make sure you know what each option does before configuring. + + If you need help with the configuration or have any questions related to Paper, + join us in our Discord or check the docs page. + + The world configuration options have been moved to their own files. + + Discord: https://discord.gg/papermc + Website: https://papermc.io/ + Docs: https://docs.papermc.io/"""; + + private static final String WORLD_DEFAULTS_HEADER = """ + This is the world defaults configuration file for Paper. + As you can see, there's a lot to configure. Some options may impact gameplay, so use + with caution, and make sure you know what each option does before configuring. + + If you need help with the configuration or have any questions related to Paper, + join us in our Discord or check the docs page. + + Configuration options here apply to all worlds, unless you specify overrides inside + the world-specific config file inside each world folder. + + Discord: https://discord.gg/papermc + Website: https://papermc.io/ + Docs: https://docs.papermc.io/"""; + + private static final String WORLD_HEADER = """ + This is a world configuration file for Paper. + This file may start empty but can be filled with settings to override ones in the config/world-defaults.yml"""; + + private static final Supplier 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) + .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(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 { + if (needsConverting(legacyConfig)) { + try { + Files.createDirectories(configDir.resolve(BACKUP_DIR)); + final Path legacyConfigBackup = configDir.resolve(BACKUP_DIR).resolve(legacyConfig.getFileName().toString() + ".old"); + Files.move(legacyConfig, legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup + convert(legacyConfigBackup, configDir, worldFolder, spigotConfig); + } catch (final IOException ex) { + throw new RuntimeException("Could not convert paper.yml to the new configuration format", ex); + } + } + try { + Files.createDirectories(configDir); + return new PaperConfigurations(configDir); + } catch (final IOException ex) { + throw new RuntimeException("Could not setup PaperConfigurations", ex); + } + } + + private static void convert(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception { + Files.createDirectories(configDir); + + final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig); + final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME)); + final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME)); + + final ConfigurationNode legacy = legacyLoader.load(); + checkState(!legacy.virtual(), "can't be virtual"); + final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt(); + + final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy(); + checkState(!legacyWorldSettings.virtual(), "can't be virtual"); + legacy.removeChild("world-settings"); + + // Apply legacy transformations before settings flatten + final YamlConfiguration spigotConfiguration = loadLegacyConfigFile(spigotConfig); // needs to change spigot config values in this transformation + LegacyPaperConfig.transformation(spigotConfiguration).apply(legacy); + spigotConfiguration.save(spigotConfig); + legacy.mergeFrom(legacy.node("settings")); // flatten "settings" to root + legacy.removeChild("settings"); + LegacyPaperConfig.toNewFormat().apply(legacy); + globalLoader.save(legacy); // save converted node to new global location + + final ConfigurationNode worldDefaults = legacyWorldSettings.node("default").copy(); + checkState(!worldDefaults.virtual()); + worldDefaults.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version); + legacyWorldSettings.removeChild("default"); + LegacyPaperWorldConfig.transformation().apply(worldDefaults); + LegacyPaperWorldConfig.toNewFormat().apply(worldDefaults); + worldDefaultsLoader.save(worldDefaults); + + legacyWorldSettings.childrenMap().forEach((world, legacyWorldNode) -> { + try { + legacyWorldNode.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version); + LegacyPaperWorldConfig.transformation().apply(legacyWorldNode); + LegacyPaperWorldConfig.toNewFormat().apply(legacyWorldNode); + ConfigurationLoaders.naturallySortedWithoutHeader(worldFolder.resolve(world.toString()).resolve(WORLD_CONFIG_FILE_NAME)).save(legacyWorldNode); // save converted node to new location + } catch (final ConfigurateException ex) { + ex.printStackTrace(); + } + }); + } + + private static boolean needsConverting(final Path legacyConfig) { + return Files.exists(legacyConfig) && Files.isRegularFile(legacyConfig); + } + + @Deprecated + public YamlConfiguration createLegacyObject(final MinecraftServer server) { + YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile()); + ConfigurationSection worlds = global.createSection("__________WORLDS__________"); + worlds.set("__defaults__", YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile())); + for (ServerLevel level : server.getAllLevels()) { + worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile())); + } + return global; + } + + @Deprecated + public static YamlConfiguration loadLegacyConfigFile(File configFile) throws Exception { + YamlConfiguration config = new YamlConfiguration(); + if (configFile.exists()) { + try { + config.load(configFile); + } catch (Exception ex) { + throw new Exception("Failed to load configuration file: " + configFile.getName(), ex); + } + } + return config; + } + + @VisibleForTesting + static ConfigurationNode createForTesting() { + ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build(); + ConfigurationOptions options = defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults())) + .serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer())); + return BasicConfigurationNode.root(options); + } +} diff --git a/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java new file mode 100644 index 0000000000000000000000000000000000000000..d69d203eea014fc9fb40a556f0771dba15fcd2ea --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/RemovedConfigurations.java @@ -0,0 +1,61 @@ +package io.papermc.paper.configuration; + +import org.spongepowered.configurate.NodePath; + +import static org.spongepowered.configurate.NodePath.path; + +interface RemovedConfigurations { + + NodePath[] REMOVED_WORLD_PATHS = { + path("elytra-hit-wall-damage"), + path("queue-light-updates"), + path("save-queue-limit-for-auto-save"), + path("max-chunk-sends-per-tick"), + path("max-chunk-gens-per-tick"), + path("fire-physics-event-for-redstone"), + path("fix-zero-tick-instant-grow-farms"), + path("bed-search-radius"), + path("lightning-strike-distance-limit"), + path("fix-wither-targeting-bug"), + path("remove-corrupt-tile-entities"), + path("allow-undead-horse-leashing"), + path("reset-arrow-despawn-timer-on-fall"), + path("seed-based-feature-search"), + path("seed-based-feature-search-loads-chunks"), + path("viewdistances.no-tick-view-distance"), + path("seed-based-feature-search"), // unneeded as of 1.18 + path("seed-based-feature-search-loads-chunks"), // unneeded as of 1.18 + path("reset-arrow-despawn-timer-on-fall"), + path("squid-spawn-height"), + path("viewdistances"), + path("use-alternate-fallingblock-onGround-detection"), + path("skip-entity-ticking-in-chunks-scheduled-for-unload"), + path("tracker-update-distance"), + path("allow-block-location-tab-completion"), + path("cache-chunk-maps"), + path("disable-mood-sounds"), + path("fix-cannons"), + path("player-blocking-damage-multiplier"), + path("remove-invalid-mob-spawner-tile-entities"), + path("use-hopper-check"), + path("use-async-lighting"), + path("tnt-explosion-volume"), + }; + + NodePath[] REMOVED_GLOBAL_PATHS = { + path("queue-light-updates-max-loss"), + path("sleep-between-chunk-saves"), + path("remove-invalid-statistics"), + path("min-chunk-load-threads"), + path("use-versioned-world"), + path("save-player-data"), // to spigot (converted) + path("log-named-entity-deaths"), // default in vanilla + path("chunk-tasks-per-tick"), // removed in tuinity merge + path("item-validation", "loc-name"), + path("commandErrorMessage"), + path("baby-zombie-movement-speed"), + path("limit-player-interactions"), + path("warnWhenSettingExcessiveVelocity") + }; + +} diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..5bcce63166657f80eaa8446d3dd64a5cba62f198 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java @@ -0,0 +1,465 @@ +package io.papermc.paper.configuration; + +import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.mojang.logging.LogUtils; +import io.papermc.paper.configuration.constraint.Constraint; +import io.papermc.paper.configuration.constraint.Constraints; +import io.papermc.paper.configuration.legacy.MaxEntityCollisionsInitializer; +import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; +import io.papermc.paper.configuration.legacy.SpawnLoadedRangeInitializer; +import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration; +import io.papermc.paper.configuration.type.BooleanOrDefault; +import io.papermc.paper.configuration.type.DoubleOrDefault; +import io.papermc.paper.configuration.type.Duration; +import io.papermc.paper.configuration.type.IntOrDefault; +import io.papermc.paper.configuration.type.fallback.ArrowDespawnRate; +import io.papermc.paper.configuration.type.fallback.AutosavePeriod; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2LongMap; +import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; +import net.minecraft.Util; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.monster.Vindicator; +import net.minecraft.world.entity.monster.Zombie; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import org.slf4j.Logger; +import org.spigotmc.SpigotWorldConfig; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Required; +import org.spongepowered.configurate.objectmapping.meta.Setting; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) +public class WorldConfiguration extends ConfigurationPart { + private static final Logger LOGGER = LogUtils.getLogger(); + static final int CURRENT_VERSION = 28; + + private transient final SpigotWorldConfig spigotConfig; + private transient final ResourceLocation worldKey; + WorldConfiguration(SpigotWorldConfig spigotConfig, ResourceLocation worldKey) { + this.spigotConfig = spigotConfig; + this.worldKey = worldKey; + } + + public boolean isDefault() { + return this.worldKey.equals(PaperConfigurations.WORLD_DEFAULTS_KEY); + } + + @Setting(Configuration.VERSION_FIELD) + public int version = CURRENT_VERSION; + + public AntiCheat anticheat; + + public class AntiCheat extends ConfigurationPart { + + public Obfuscation obfuscation; + + public class Obfuscation extends ConfigurationPart { + public Items items = new Items(); + public class Items extends ConfigurationPart { + public boolean hideItemmeta = false; + public boolean hideDurability = false; + } + } + + public AntiXRay antiXray; + + public class AntiXRay extends ConfigurationPart { + public boolean enabled = false; + public ChunkPacketBlockControllerAntiXray.EngineMode engineMode = ChunkPacketBlockControllerAntiXray.EngineMode.HIDE; + public int maxBlockHeight = 64; + public int updateRadius = 2; + public boolean lavaObscures = false; + public boolean usePermission = false; + public List 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; + public Reference2IntMap spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1))); + 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/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..0b235ebe6e79d7aa420d6b8a52aedb3a4d8b6629 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/serializer/TableSerializer.java @@ -0,0 +1,89 @@ +package io.papermc.paper.configuration.serializer; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Table; +import io.leangen.geantyref.TypeFactory; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.BasicConfigurationNode; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Objects; + +public class TableSerializer implements TypeSerializer> { + 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()) { + rowKeySerializer.deserialize(rowType, rowKeyNode.set(key)); + final Map map = columnValueSerializer.deserialize(mapType, node.node(rowKeyNode.raw())); + map.forEach((column, value) -> table.put((R) rowKeyNode.raw(), column, value)); + } + } + + @Override + public void serialize(final Type type, @Nullable final Table table, final ConfigurationNode node) throws SerializationException { + if (table != null) { + this.serialize0(table, (ParameterizedType) type, node); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private 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..62e41812b0df4fc548f97273215a92b993ebf1d7 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/transformation/global/LegacyPaperConfig.java @@ -0,0 +1,224 @@ +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(); + } else { + LOGGER.warn("Could not convert spigot-mapped packet class name {} because no mappings were found in the jar", keyValue); + } + 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..37aa2873eb7fbfb9cfbf890e5ca2a3965f8d612f --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java @@ -0,0 +1,64 @@ +package io.papermc.paper.configuration.type; + +import org.apache.commons.lang3.math.NumberUtils; +import org.spongepowered.configurate.serialize.ScalarSerializer; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.lang.reflect.Type; +import java.util.OptionalDouble; +import java.util.function.Predicate; + +public final class DoubleOrDefault { + private static final String DEFAULT_VALUE = "default"; + public static final DoubleOrDefault USE_DEFAULT = new DoubleOrDefault(OptionalDouble.empty()); + public static final ScalarSerializer 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 Double num) { + return new DoubleOrDefault(OptionalDouble.of(num)); + } + throw new SerializationException(obj + " is of an unexpected type " + type); + } + + @Override + protected Object serialize(final DoubleOrDefault item, final Predicate> 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/IntOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java new file mode 100644 index 0000000000000000000000000000000000000000..18a77c5694dc9739c7e2b52deb7dbfebb01b6c38 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/type/IntOrDefault.java @@ -0,0 +1,49 @@ +package io.papermc.paper.configuration.type; + +import java.lang.reflect.Type; +import java.util.OptionalInt; +import java.util.function.Predicate; +import org.apache.commons.lang3.math.NumberUtils; +import org.spongepowered.configurate.serialize.ScalarSerializer; +import org.spongepowered.configurate.serialize.SerializationException; + +public record IntOrDefault(OptionalInt value) { + private static final String DEFAULT_VALUE = "default"; + public static final IntOrDefault USE_DEFAULT = new IntOrDefault(OptionalInt.empty()); + public static final ScalarSerializer 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 Integer num) { + return new IntOrDefault(OptionalInt.of(num)); + } + throw new SerializationException(obj + "(" + type + ") is not a integer or '" + DEFAULT_VALUE + "'"); + } + + @Override + protected Object serialize(final IntOrDefault item, final Predicate> 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..d279ef02a44e2a8fc971cbf8ec816444271ccfc9 --- /dev/null +++ b/src/main/java/io/papermc/paper/configuration/type/fallback/FallbackValue.java @@ -0,0 +1,101 @@ +package io.papermc.paper.configuration.type.fallback; + +import com.google.common.base.Preconditions; +import net.minecraft.server.MinecraftServer; +import org.apache.commons.lang3.math.NumberUtils; +import org.spigotmc.SpigotWorldConfig; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.util.Map; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.Set; +import java.util.function.Supplier; + +public sealed abstract class FallbackValue permits FallbackValue.Int { + + private static final String DEFAULT_VALUE = "default"; + static final ContextKey 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 6cc81495d8d09ff1fbb09f2e63a16ec4fa6138ec..73662a463fc9d38b39c4c779fcfaa09a4689d704 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -279,6 +279,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { AtomicReference atomicreference = new AtomicReference(); @@ -369,6 +370,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 1d94c0fbdead83155aefc8d4a16dbcb95b3c9838..42be8ab024e5c889b2a114f1098e1bedd1193d80 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -861,6 +861,7 @@ public final class CraftServer implements Server { } org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot + this.console.paperConfigurations.reloadConfigs(this.console); for (ServerLevel world : this.console.getAllLevels()) { world.serverLevelData.setDifficulty(config.difficulty); world.setSpawnSettings(config.spawnMonsters, config.spawnAnimals); diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java index e5008c75054df38356af193fd049110d7d56e2d4..c694c6dfed0b3aa098b1822676e39bd3eb04b45a 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java @@ -129,6 +129,14 @@ public class Main { .defaultsTo(new File("spigot.yml")) .describedAs("Yml file"); // Spigot End + + // Paper Start + acceptsAll(asList("paper", "paper-settings"), "File for paper settings") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(new File("paper.yml")) + .describedAs("Yml file"); + // Paper end } }; diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java index a96cb7a5f7c94cd9a46b31cf8ec90b544221557b..7c35d86eac0d69ba4be48faf364fd6dc84fa7e87 100644 --- a/src/main/java/org/spigotmc/SpigotConfig.java +++ b/src/main/java/org/spigotmc/SpigotConfig.java @@ -96,7 +96,7 @@ public class SpigotConfig } } - static void readConfig(Class clazz, Object instance) + public static void readConfig(Class clazz, Object instance) // Paper - package-private -> public { for ( Method method : clazz.getDeclaredMethods() ) { diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java index a04da0a7d690fe3fcf10810b4e8c92a8ae027b86..feef74e3a6d50344245c4a61ece5b2194af1072f 100644 --- a/src/main/java/org/spigotmc/SpigotWorldConfig.java +++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java @@ -58,8 +58,14 @@ public class SpigotWorldConfig public int getInt(String path, int def) { - this.config.addDefault( "world-settings.default." + path, def ); - return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path ) ); + // Paper start - get int without setting default + return this.getInt(path, def, true); + } + public int getInt(String path, int def, boolean setDef) + { + if (setDef) this.config.addDefault( "world-settings.default." + path, def ); + return this.config.getInt( "world-settings." + this.worldName + "." + path, this.config.getInt( "world-settings.default." + path, def ) ); + // Paper end } public List getList(String path, T def) diff --git a/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java new file mode 100644 index 0000000000000000000000000000000000000000..0396589795da1f83ddf62426236dde9a3afa1376 --- /dev/null +++ b/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java @@ -0,0 +1,20 @@ +package io.papermc.paper.configuration; + +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; + +public final class GlobalConfigTestingBase { + + public static void setupGlobalConfigForTest() { + //noinspection ConstantConditions + if (GlobalConfiguration.get() == null) { + ConfigurationNode node = PaperConfigurations.createForTesting(); + try { + GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class); + GlobalConfiguration.set(globalConfiguration); + } catch (SerializationException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/test/java/org/bukkit/support/AbstractTestingBase.java b/src/test/java/org/bukkit/support/AbstractTestingBase.java index b58962026c739ed06fbe0f702cb2877fbfa7b2e4..7de91b0b669664be3406b77a453d517604170404 100644 --- a/src/test/java/org/bukkit/support/AbstractTestingBase.java +++ b/src/test/java/org/bukkit/support/AbstractTestingBase.java @@ -45,6 +45,7 @@ public abstract class AbstractTestingBase { DummyServer.setup(); DummyEnchantments.setup(); + io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(); // Paper ImmutableList.Builder builder = ImmutableList.builder(); for (Material m : Material.values()) {