Compare commits

...
Author SHA1 Message Date
Jake Potrebic 17fbe152a6 Refactor early config loading
Now its still part of GlobalConfiguration which means
settings can live in their natural places instead of
a separate section.
2026-10-25 16:45:52 -07:00
Nassim Jahnke 8ac3459533 Add back commands.yml 2026-10-25 08:40:15 +01:00
Nassim Jahnke b72728d717 Copy in Geyser yaml loader code 2026-10-25 08:25:58 +01:00
Nassim JahnkeandJake Potrebic df17d86a37 Merge Spigot/Bukkit configs into Paper configs
Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>
2026-10-24 16:31:53 +02:00
71 changed files with 5503 additions and 1691 deletions

No files matched your search

+8 -55
View File
@@ -632,7 +632,10 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
* @param spawnCategory the category of spawn
* @return the default ticks per {@link SpawnCategory} mobs spawn value
* @throws IllegalArgumentException if the category is {@link SpawnCategory#MISC}
* @deprecated this is configured per world; use {@link World#getTicksPerSpawns(SpawnCategory)}.
* This returns the primary world's value.
*/
@Deprecated(forRemoval = true, since = "26.3")
public int getTicksPerSpawns(@NotNull SpawnCategory spawnCategory);
/**
@@ -1908,7 +1911,10 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
*
* @param spawnCategory the category spawn
* @return the {@link SpawnCategory} spawn limit
* @deprecated this is configured per world; use {@link World#getSpawnLimit(SpawnCategory)}.
* This returns the primary world's value.
*/
@Deprecated(forRemoval = true, since = "26.3")
int getSpawnLimit(@NotNull SpawnCategory spawnCategory);
/**
@@ -2421,60 +2427,6 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
// Spigot start
public class Spigot {
/**
* @deprecated Server config options may be renamed or removed without notice. Prefer using existing API
* wherever possible, rather than directly reading from a server config.
*
* @see #getServerConfig()
* @return The server's spigot config.
*/
@Deprecated(since = "1.21.4", forRemoval = true)
@NotNull
public org.bukkit.configuration.file.YamlConfiguration getConfig() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* @deprecated Server config options may be renamed or removed without notice. Prefer using existing API
* wherever possible, rather than directly reading from a server config.
*
* @see #getServerConfig()
* @return The server's bukkit config.
*/
// Paper start
@Deprecated(since = "1.21.4", forRemoval = true)
@NotNull
public org.bukkit.configuration.file.YamlConfiguration getBukkitConfig() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* @deprecated Server config options may be renamed or removed without notice. Prefer using existing API
* wherever possible, rather than directly reading from a server config.
*
* @see #getServerConfig()
* @return The server's spigot config.
*/
@Deprecated(since = "1.21.4", forRemoval = true)
@NotNull
public org.bukkit.configuration.file.YamlConfiguration getSpigotConfig() {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* @deprecated Server config options may be renamed or removed without notice. Prefer using existing API
* wherever possible, rather than directly reading from a server config.
*
* @see #getServerConfig()
* @return The server's paper config.
*/
@Deprecated(since = "1.21.4", forRemoval = true)
@NotNull
public org.bukkit.configuration.file.YamlConfiguration getPaperConfig() {
throw new UnsupportedOperationException("Not supported yet.");
}
// Paper end
/**
* Sends the component to the player
*
@@ -2521,6 +2473,7 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
*/
void restart();
@Deprecated(forRemoval = true, since = "26.3")
void reloadPermissions(); // Paper
boolean reloadCommandAliases(); // Paper
@@ -2541,7 +2494,7 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
* @deprecated use {@link #permissionMessage()}
*/
@NotNull
@Deprecated
@Deprecated(forRemoval = true)
String getPermissionMessage();
/**
@@ -3472,7 +3472,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* world
* <p>
* <b>Note:</b> If set to a negative number the world will use the
* server-wide spawn limit instead.
* spawn limit from its configuration instead.
*
* @param limit the new mob limit
* @deprecated Deprecated in favor of {@link #setSpawnLimit(SpawnCategory, int)}
@@ -3499,7 +3499,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* world
* <p>
* <b>Note:</b> If set to a negative number the world will use the
* server-wide spawn limit instead.
* spawn limit from its configuration instead.
*
* @param limit the new mob limit
* @deprecated Deprecated in favor of {@link #getSpawnLimit(SpawnCategory)}
@@ -3526,7 +3526,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* this world
* <p>
* <b>Note:</b> If set to a negative number the world will use the
* server-wide spawn limit instead.
* spawn limit from its configuration instead.
*
* @param limit the new mob limit
* @deprecated Deprecated in favor of {@link #setSpawnLimit(SpawnCategory, int)}
@@ -3553,7 +3553,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* this world
* <p>
* <b>Note:</b> If set to a negative number the world will use the
* server-wide spawn limit instead.
* spawn limit from its configuration instead.
*
* @param limit the new mob limit
* @deprecated Deprecated in favor of {@link #setSpawnLimit(SpawnCategory, int)}
@@ -3580,7 +3580,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* in this world
* <p>
* <b>Note:</b> If set to a negative number the world will use the
* server-wide spawn limit instead.
* spawn limit from its configuration instead.
*
* @param limit the new mob limit
* @deprecated Deprecated in favor of {@link #setSpawnLimit(SpawnCategory, int)}
@@ -3607,7 +3607,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* this world
* <p>
* <b>Note:</b> If set to a negative number the world will use the
* server-wide spawn limit instead.
* spawn limit from its configuration instead.
*
* @param limit the new mob limit
* @deprecated Deprecated in favor of {@link #setSpawnLimit(SpawnCategory, int)}
@@ -3631,7 +3631,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* this world
* <p>
* <b>Note:</b> If set to a negative number the world will use the
* server-wide spawn limit instead.
* spawn limit from its configuration instead.
*
* @param spawnCategory the entity category
* @param limit the new mob limit
+1 -1
View File
@@ -136,7 +136,7 @@ dependencies {
implementation("io.netty:netty-codec-haproxy:4.2.16.Final") // Add support for proxy protocol
implementation("org.apache.logging.log4j:log4j-iostreams:2.26.0")
implementation("org.ow2.asm:asm-commons:9.9.1")
implementation("org.spongepowered:configurate-yaml:4.2.0")
implementation("org.spongepowered:configurate-core:4.2.0") // plus GeyserMC's yaml loader with comments in io.papermc.paper.configuration.yaml
// Deps that were previously in the API but have now been moved here for backwards compat, eventually to be removed
runtimeOnly("commons-lang:commons-lang:2.6")
@@ -12,6 +12,8 @@ import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.spigotmc.RestartCommand;
import org.spigotmc.TicksPerSecondCommand;
@DefaultQualifier(NonNull.class)
public final class PaperCommands {
@@ -23,6 +25,8 @@ public final class PaperCommands {
public static void registerCommands(final MinecraftServer server) {
COMMANDS.put("paper", new PaperCommand("paper"));
COMMANDS.put("tps", new TicksPerSecondCommand("tps")); // from spigot
COMMANDS.put("restart", new RestartCommand("restart")); // from spigot
COMMANDS.forEach((s, command) -> {
server.server.getCommandMap().register(s, "Paper", command);
@@ -1,7 +1,6 @@
package io.papermc.paper.command.subcommands;
import io.papermc.paper.command.PaperSubcommand;
import net.minecraft.server.MinecraftServer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.craftbukkit.CraftServer;
@@ -24,9 +23,9 @@ public final class ReloadCommand implements PaperSubcommand {
Command.broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", RED));
Command.broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", RED));
MinecraftServer server = ((CraftServer) sender.getServer()).getServer();
server.paperConfigurations.reloadConfigs(server);
server.server.reloadCount++;
final CraftServer server = (CraftServer) sender.getServer();
server.reloadPaperConfiguration();
server.reloadCount++;
Command.broadcastCommandMessage(sender, text("Paper config reload complete.", GREEN));
}
@@ -2,11 +2,10 @@ 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;
public static final int FINAL_LEGACY_SPIGOT_VERSION = 13;
private Configuration() {
}
@@ -1,10 +1,11 @@
package io.papermc.paper.configuration;
import io.papermc.paper.configuration.yaml.BlankLineStyle;
import io.papermc.paper.configuration.yaml.NodeStyle;
import io.papermc.paper.configuration.yaml.YamlConfigurationLoader;
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() {
@@ -15,6 +16,8 @@ public final class ConfigurationLoaders {
.indent(2)
.nodeStyle(NodeStyle.BLOCK)
.headerMode(HeaderMode.PRESET)
.commentsEnabled(true)
.blankLineStyle(BlankLineStyle.AFTER_NESTED)
.defaultOptions(options -> options.mapFactory(MapFactories.sortedNatural()));
}
@@ -5,6 +5,8 @@ import com.mojang.logging.LogUtils;
import io.leangen.geantyref.TypeToken;
import io.papermc.paper.configuration.constraint.Constraint;
import io.papermc.paper.configuration.constraint.Constraints;
import io.papermc.paper.configuration.transformation.ServerPropertiesAccess;
import io.papermc.paper.configuration.yaml.YamlConfigurationLoader;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.AccessDeniedException;
@@ -14,9 +16,11 @@ import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import net.minecraft.core.RegistryAccess;
import net.minecraft.resources.Identifier;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.gamerules.GameRules;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
@@ -29,7 +33,6 @@ 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;
public abstract class Configurations<G, W> {
@@ -59,10 +62,18 @@ public abstract class Configurations<G, W> {
}
protected ObjectMapper.Factory.Builder createObjectMapper() {
return ObjectMapper.factoryBuilder()
return defaultObjectMapperFactoryBuilder();
}
static ObjectMapper.Factory.Builder defaultObjectMapperFactoryBuilder() {
return addDefaultConstraints(ObjectMapper.factoryBuilder());
}
static ObjectMapper.Factory.Builder addDefaultConstraints(final ObjectMapper.Factory.Builder builder) {
return builder
.addConstraint(Constraint.class, new Constraint.Factory())
.addConstraint(Constraints.Min.class, Number.class, new Constraints.Min.Factory())
.addConstraint(Constraints.Max.class, Number.class, new Constraints.Max.Factory());
.addConstraint(Constraints.Min.class, new Constraints.Min.Factory())
.addConstraint(Constraints.Max.class, new Constraints.Max.Factory());
}
protected YamlConfigurationLoader.Builder createLoaderBuilder() {
@@ -80,7 +91,7 @@ public abstract class Configurations<G, W> {
}
@MustBeInvokedByOverriders
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(RegistryAccess registryAccess) {
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(final ContextMap contextMap) {
return this.createLoaderBuilder();
}
@@ -103,8 +114,8 @@ public abstract class Configurations<G, W> {
};
}
public G initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException {
return this.initializeGlobalConfiguration(registryAccess, creator(this.globalConfigClass, true));
public G initializeGlobalConfiguration(final DedicatedServer server) throws ConfigurateException {
return this.initializeGlobalConfiguration(this.createGlobalContextMap(server).build(), creator(this.globalConfigClass, true));
}
private void trySaveFileNode(YamlConfigurationLoader loader, ConfigurationNode node, String filename) throws ConfigurateException {
@@ -117,9 +128,9 @@ public abstract class Configurations<G, W> {
}
}
protected G initializeGlobalConfiguration(final RegistryAccess registryAccess, final CheckedFunction<ConfigurationNode, G, SerializationException> creator) throws ConfigurateException {
protected G initializeGlobalConfiguration(final ContextMap contextMap, final CheckedFunction<ConfigurationNode, G, SerializationException> creator) throws ConfigurateException {
final Path configFile = this.globalFolder.resolve(this.globalConfigFileName);
final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder(registryAccess)
final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder(contextMap)
.defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build()))
.path(configFile)
.build();
@@ -132,9 +143,9 @@ public abstract class Configurations<G, W> {
node = loader.load();
this.verifyGlobalConfigVersion(node);
}
this.applyGlobalConfigTransformations(node);
this.applyGlobalConfigTransformations(contextMap, node);
final G instance = creator.apply(node);
trySaveFileNode(loader, node, configFile.toString());
this.trySaveFileNode(loader, node, configFile.toString());
return instance;
}
@@ -148,7 +159,14 @@ public abstract class Configurations<G, W> {
}
}
protected void applyGlobalConfigTransformations(final ConfigurationNode node) throws ConfigurateException {
protected void applyGlobalConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException {
}
@MustBeInvokedByOverriders
protected ContextMap.Builder createGlobalContextMap(final DedicatedServer server) {
return ContextMap.builder()
.put(REGISTRY_ACCESS, server.registryAccess())
.put(SERVER_PROPERTIES, ServerPropertiesAccess.of(server));
}
@MustBeInvokedByOverriders
@@ -268,8 +286,12 @@ public abstract class Configurations<G, W> {
}
private UnaryOperator<ConfigurationOptions> applyObjectMapperFactory(final ObjectMapper.Factory factory) {
return options -> options.serializers(builder -> builder
.register(this::isConfigType, factory.asTypeSerializer())
return options -> applyObjectMapperFactory(options, this::isConfigType, factory);
}
static ConfigurationOptions applyObjectMapperFactory(final ConfigurationOptions options, final Predicate<Type> isConfigType, final ObjectMapper.Factory factory) {
return options.serializers(builder -> builder
.register(isConfigType, factory.asTypeSerializer())
.registerAnnotatedObjects(factory));
}
@@ -342,6 +364,7 @@ public abstract class Configurations<G, W> {
public static final ContextKey<Void> FIRST_DEFAULT = new ContextKey<>(Void.class, "first default");
public static final ContextKey<RegistryAccess> REGISTRY_ACCESS = new ContextKey<>(RegistryAccess.class, "registry access");
public static final ContextKey<GameRules> GAME_RULES = new ContextKey<>(GameRules.class, "game rules");
public static final ContextKey<ServerPropertiesAccess> SERVER_PROPERTIES = new ContextKey<>(ServerPropertiesAccess.class, "server properties");
public record ContextKey<T>(TypeToken<T> type, String name) {
@@ -1,22 +1,38 @@
package io.papermc.paper.configuration;
import com.mojang.logging.LogUtils;
import io.papermc.paper.configuration.constraint.Constraint;
import io.papermc.paper.configuration.constraint.Constraints;
import io.papermc.paper.configuration.mapping.LoadEarly;
import io.papermc.paper.configuration.mapping.MergeMap;
import io.papermc.paper.configuration.serializer.collection.map.WriteKeyBack;
import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import io.papermc.paper.util.sanitizer.ItemObfuscationBinding;
import io.papermc.paper.util.sanitizer.OversizedItemComponentSanitizer;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.attributes.RangedAttribute;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.bukkit.Warning;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@@ -28,8 +44,9 @@ import org.spongepowered.configurate.objectmapping.meta.Setting;
@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
public class GlobalConfiguration extends ConfigurationPart {
private static final Logger LOGGER = LogUtils.getLogger();
static final int CURRENT_VERSION = 31; // (when you change the version, change the comment, so it conflicts on rebases): allow-nether property to config
static final int CURRENT_VERSION = 32; // (when you change the version, change the comment, so it conflicts on rebases): merge spigot.yml and bukkit.yml
private static GlobalConfiguration instance;
private static @Nullable Level levelBeforeDebug; // root log level to restore once logging.debug is turned off again
public static boolean isFirstStart = false;
public static GlobalConfiguration get() {
return instance;
@@ -90,10 +107,18 @@ public class GlobalConfiguration extends ConfigurationPart {
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 whitelist = Component.translatable("multiplayer.disconnect.not_whitelisted");
public Component serverFull = Component.translatable("multiplayer.disconnect.server_full");
public Component outdatedClient = Component.translatable("multiplayer.disconnect.outdated_client");
public Component outdatedServer = Component.translatable("multiplayer.disconnect.outdated_server");
public Component restart = Component.text("Server is restarting");
}
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 boolean sendCommandParseFailureMessage = true;
@Comment("Broadcast to players when the server shuts down.")
public Component shutdown = Component.text("Server closed");
}
public Spark spark;
@@ -109,6 +134,7 @@ public class GlobalConfiguration extends ConfigurationPart {
public BungeeCord bungeeCord;
public class BungeeCord extends ConfigurationPart {
public boolean enabled = false;
public boolean onlineMode = true;
}
@@ -136,7 +162,15 @@ public class GlobalConfiguration extends ConfigurationPart {
}
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);
return org.bukkit.Bukkit.getOnlineMode() || (this.bungeeCord.enabled && this.bungeeCord.onlineMode) || (this.velocity.enabled && this.velocity.onlineMode);
}
@PostProcess
private void postProcess() {
if (this.bungeeCord.enabled && this.velocity.enabled) {
LOGGER.error("Both BungeeCord and Velocity forwarding are enabled, but only one can be used. Disabling BungeeCord...");
this.bungeeCord.enabled = false;
}
}
}
@@ -151,6 +185,9 @@ public class GlobalConfiguration extends ConfigurationPart {
public Watchdog watchdog;
public class Watchdog extends ConfigurationPart {
public int timeoutSeconds = 60;
public boolean restartOnCrash = true;
public String restartScript = "./start.sh";
public int earlyWarningEvery = 5000;
public int earlyWarningDelay = 10000;
}
@@ -163,6 +200,13 @@ public class GlobalConfiguration extends ConfigurationPart {
public int recipeSpamIncrement = 1;
public int recipeSpamLimit = 20;
public IntOr.Disabled incomingPacketThreshold = new IntOr.Disabled(OptionalInt.of(300));
@Comment("Commands that do not count towards the chat spam limit, matched by prefix.")
public Commands commands;
public class Commands extends ConfigurationPart {
public boolean enabled = false;
public Set<String> exclusions = Set.of("/skill");
}
}
public UnsupportedSettings unsupportedSettings;
@@ -193,6 +237,12 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean suggestPlayerNamesWhenNullTabCompletions = true;
@Comment("Allow mounting entities to a player in the Vanilla '/ride' command.")
public boolean rideCommandAllowPlayerAsVehicle = false;
@Comment("Send commands with namespace prefixes to clients")
public boolean sendNamespacedCommands = true;
@Comment("Send tab completions to clients")
public boolean tabCompletion = true;
@Comment("Hide command block output from the console, regardless of the commandBlockOutput game rule.")
public boolean silentCommandBlockConsole = false;
}
public Time time;
@@ -201,6 +251,38 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean affectsAllWorlds = false;
}
public Logging logging;
public class Logging extends ConfigurationPart {
@Comment("Raise the root logger to trace level. Very noisy; only useful when debugging the server itself.")
public boolean debug = false;
@Comment("Log the execution of commands")
public boolean commandExecution = true;
@Comment("Log the deaths of villagers")
public boolean villagerDeaths = true;
@Comment("Log the deaths of named living entities")
public boolean namedLivingEntityDeaths = true;
@PostProcess
private void postProcess() {
final LoggerContext context = (LoggerContext) LogManager.getContext(false);
final org.apache.logging.log4j.core.config.Configuration configuration = context.getConfiguration();
final org.apache.logging.log4j.core.config.LoggerConfig root = configuration.getLoggerConfig(LogManager.ROOT_LOGGER_NAME);
if (this.debug && levelBeforeDebug == null && !LogManager.getRootLogger().isTraceEnabled()) {
levelBeforeDebug = root.getLevel();
root.setLevel(Level.ALL);
context.updateLoggers(configuration);
} else if (!this.debug && levelBeforeDebug != null) {
root.setLevel(levelBeforeDebug);
levelBeforeDebug = null;
context.updateLoggers(configuration);
}
if (LogManager.getRootLogger().isTraceEnabled()) {
LOGGER.info("Debug logging is enabled");
}
}
}
public Scoreboards scoreboards;
public class Scoreboards extends ConfigurationPart {
@@ -215,6 +297,8 @@ public class GlobalConfiguration extends ConfigurationPart {
public int ioThreads = -1;
public int workerThreads = -1;
@Comment("Ticks a chunk stays loaded after a plugin ticket for it is added.")
public int pluginTicketTimeout = 600;
@PostProcess
private void postProcess() {
@@ -270,6 +354,16 @@ public class GlobalConfiguration extends ConfigurationPart {
}
}
@Comment("""
Plugin-provided generators, keyed by world name. Read while a world is being created, before its
own config exists, which is why these live here rather than in the world config.""")
public Map<String, WorldGenerators> worldGenerators = Map.of();
@ConfigSerializable
public record WorldGenerators(@Nullable String generator, @Nullable String biomeProvider) {
}
public Collisions collisions;
public class Collisions extends ConfigurationPart {
@@ -277,17 +371,26 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean sendFullPosForHardCollidingEntities = true;
}
public PlayerAutoSave playerAutoSave;
public Players players;
public class Players extends ConfigurationPart {
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;
public boolean disableSaving = false;
public AutoSave autoSave;
public int userCacheSize = 1000;
public boolean saveUserCacheOnStopOnly = false;
@Constraint(Constraints.Positive.class)
public int sampleCount = 12;
public int connectionShuffle = 0;
public class AutoSave 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;
}
return this.maxPerTick;
}
}
@@ -323,10 +426,14 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean loadPermissionsYmlBeforePlugins = true;
@Constraints.Min(4)
public int regionFileCacheSize = 256;
@Comment("""
Whether chunks generated before 1.18 get the extended world height applied when they are next loaded.
Global rather than per-world: it is also consulted by the world upgrader and the legacy structure
file fix, which run over region files without a loaded world.""")
public boolean belowZeroGenerationInExistingChunks = true;
@Comment("See https://luckformula.emc.gs")
public boolean useAlternativeLuckFormula = false;
public boolean useDimensionTypeForCustomSpawners = false;
public boolean strictAdvancementDimensionCheck = false;
public IntOr.Default compressionLevel = IntOr.Default.USE_DEFAULT;
@Comment("Defines the leniency distance added on the server to the interaction range of a player when validating interact packets.")
public DoubleOr.Default clientInteractionLeniencyDistance = DoubleOr.Default.USE_DEFAULT;
@@ -335,10 +442,46 @@ public class GlobalConfiguration extends ConfigurationPart {
public IntOr.Default xpOrbGroupsPerArea = IntOr.Default.USE_DEFAULT;
@Comment("See Fix MC-163962; prevent villager demand from going negative.")
public boolean preventNegativeVillagerDemand = false;
@Comment("""
Ticks between server-wide saves of global data and player data.
Per-world chunk saving is configured separately, with chunks.auto-save-interval in the world config.""")
public int autoSaveInterval = 6000;
@Comment("Whether the nether dimension is enabled and will be loaded.")
public boolean enableNether = true;
@Comment("Whether the end dimension is enabled and will be loaded.")
public boolean enableEnd = true;
@Comment("File that plugin permissions are loaded from, relative to the server root.")
public String permissionsFile = "permissions.yml";
@LoadEarly
@Comment("""
The directory inside the plugins folder that plugin updates are loaded from. Set to an empty string to disable auto updating.
Read before plugins are discovered, so changes require a restart.""")
public String updateFolder = "update";
@LoadEarly
@Comment("""
The directory worlds are stored in, relative to the server root.
Read before the world folder is opened, so changes require a restart. Overridden by the --universe launch argument.""")
public String worldContainer = ".";
@Comment("Milliseconds a connecting player must wait before reconnecting. Set to -1 to disable.")
public int connectionThrottle = 4000;
@Comment("Whether the plugin list is included in GS4 query responses.")
public boolean queryPlugins = true;
@Comment("""
Whether deprecation warnings are printed when a plugin uses a deprecated API.
DEFAULT defers to the plugin's own author-declared preference.""")
public Warning.WarningState deprecatedVerbose = Warning.WarningState.DEFAULT;
@Comment("Lowest plugin API version the server will load. 'none' allows any.")
public String minimumApi = "none";
@Comment("Cache map colours between renders. Uses more memory but is faster.")
public boolean useMapColorCache = true;
@Comment("Fix for MC-301114. This removes the oldest combat entry when it hits the cap, to fix a memory leak on constant entity damage.")
public IntOr.Disabled maxTrackingCombatEntries = new IntOr.Disabled(OptionalInt.of(10240));
public int nettyThreads = 4;
@PostProcess
private void postProcess() {
System.setProperty("io.netty.eventLoopThreads", Integer.toString(this.nettyThreads));
}
}
public BlockUpdates blockUpdates;
@@ -354,6 +497,9 @@ public class GlobalConfiguration extends ConfigurationPart {
public class Anticheat extends ConfigurationPart {
public double movedWronglyThreshold = 0.0625D;
public double movedTooQuicklyMultiplier = 10.0D;
public Obfuscation obfuscation;
public class Obfuscation extends ConfigurationPart {
@@ -392,4 +538,44 @@ public class GlobalConfiguration extends ConfigurationPart {
public class UpdateChecker extends ConfigurationPart {
public boolean enabled = true;
}
public Advancements advancements;
public class Advancements extends ConfigurationPart {
public boolean strictDimensionCheck = false;
public boolean disableSaving = false;
@LoadEarly
@Comment("""
Advancements that will not be loaded at all. Accepts '*' to disable every advancement,
a namespace to disable all advancements in it, or individual advancement keys.
Read before datapacks are loaded, so changes require a restart.""")
public Set<String> disabled = Set.of("minecraft:story/disabled");
}
public Stats stats;
public class Stats extends ConfigurationPart {
public boolean disableSaving = false;
public Object2IntMap<Holder<Identifier>> forcedCustomStatValues = new Object2IntOpenHashMap<>();
}
public AttributesSection attributes;
public class AttributesSection extends ConfigurationPart {
@ConfigSerializable
public record AttributeOverride(double max) {
}
@MergeMap
public Map<Holder<Attribute>, AttributeOverride> overrides = Stream.of(Attributes.MAX_ABSORPTION, Attributes.MAX_HEALTH, Attributes.MOVEMENT_SPEED, Attributes.ATTACK_DAMAGE)
.collect(Collectors.toMap(Function.identity(), a -> new AttributeOverride(((RangedAttribute) a.value()).maxValue)));
@PostProcess
private void postProcess() {
this.overrides.forEach((attribute, override) -> {
if (attribute.value() instanceof final RangedAttribute ranged) {
ranged.maxValue = override.max();
} else {
LOGGER.warn("Ignoring attribute max override for {}, it is not a ranged attribute", attribute.getRegisteredName());
}
});
}
}
}
@@ -1,13 +1,12 @@
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.mapping.Definition;
import io.papermc.paper.configuration.mapping.FieldProcessor;
import io.papermc.paper.configuration.mapping.InnerClassFieldDiscoverer;
import io.papermc.paper.configuration.mapping.LoadEarly;
import io.papermc.paper.configuration.mapping.MergeMap;
import io.papermc.paper.configuration.serializer.ComponentSerializer;
import io.papermc.paper.configuration.serializer.EnumValueSerializer;
@@ -20,15 +19,16 @@ import io.papermc.paper.configuration.serializer.collection.map.FastutilMapSeria
import io.papermc.paper.configuration.serializer.collection.map.MapSerializer;
import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer;
import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer;
import io.papermc.paper.configuration.transformation.ContextualTransformation;
import io.papermc.paper.configuration.transformation.Transformations;
import io.papermc.paper.configuration.transformation.global.LegacyPaperConfig;
import io.papermc.paper.configuration.transformation.global.versioned.V29_LogIPs;
import io.papermc.paper.configuration.transformation.global.versioned.V30_PacketIds;
import io.papermc.paper.configuration.transformation.global.versioned.V31_AllowNetherPropertiesToConfig;
import io.papermc.paper.configuration.transformation.global.versioned.V32_SpigotConfigCleanup;
import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration;
import io.papermc.paper.configuration.transformation.world.LegacyPaperWorldConfig;
import io.papermc.paper.configuration.transformation.world.versioned.V29_ZeroWorldHeight;
import io.papermc.paper.configuration.transformation.world.versioned.V30_RenameFilterNbtFromSpawnEgg;
import io.papermc.paper.configuration.transformation.world.versioned.V32_WorldConfigCleanup;
import io.papermc.paper.configuration.type.BooleanOrDefault;
import io.papermc.paper.configuration.type.DespawnRange;
import io.papermc.paper.configuration.type.Duration;
@@ -37,54 +37,54 @@ import io.papermc.paper.configuration.type.EngineMode;
import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer;
import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import io.papermc.paper.configuration.type.number.LongOr;
import io.papermc.paper.configuration.yaml.YamlConfigurationLoader;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
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 it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.function.Supplier;
import net.kyori.adventure.key.Key;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.levelgen.feature.Feature;
import org.apache.commons.lang3.RandomStringUtils;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.VisibleForTesting;
import org.jspecify.annotations.Nullable;
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.FieldDiscoverer;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import org.spongepowered.configurate.util.NamingSchemes;
import static com.google.common.base.Preconditions.checkState;
import static io.leangen.geantyref.GenericTypeReflector.erase;
@SuppressWarnings("Convert2Diamond")
@@ -95,7 +95,6 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
static final String WORLD_DEFAULTS_CONFIG_FILE_NAME = "paper-world-defaults.yml";
static final String WORLD_CONFIG_FILE_NAME = "paper-world.yml";
public static final String CONFIG_DIR = "config";
private static final String BACKUP_DIR ="legacy-backup";
private static final String GLOBAL_HEADER = String.format("""
This is the global configuration file for Paper.
@@ -141,22 +140,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
map.require(WORLD_KEY)
);
private static final String MOVED_NOTICE = """
The global and world default configuration files have moved to %s
and the world-specific configuration file has been moved inside
the respective world folder.
See https://docs.papermc.io/paper/configuration for more information.
""";
@VisibleForTesting
public static final Supplier<SpigotWorldConfig> SPIGOT_WORLD_DEFAULTS = Suppliers.memoize(() -> new SpigotWorldConfig(RandomStringUtils.randomAlphabetic(255), Key.key(RandomStringUtils.randomAlphabetic(255).toLowerCase(Locale.ROOT))) {
@Override // override to ensure "verbose" is false
public void init() {
SpigotConfig.readConfig(SpigotWorldConfig.class, this);
}
});
public static final ContextKey<Supplier<SpigotWorldConfig>> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken<Supplier<SpigotWorldConfig>>() {}, "spigot world config");
public PaperConfigurations(final Path globalFolder) {
@@ -186,6 +170,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
.register(new ComponentSerializer())
.register(IntOr.Default.SERIALIZER)
.register(IntOr.Disabled.SERIALIZER)
.register(LongOr.Default.SERIALIZER)
.register(DoubleOr.Default.SERIALIZER)
.register(DoubleOr.Disabled.SERIALIZER)
.register(BooleanOrDefault.SERIALIZER)
@@ -198,54 +183,67 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
@Override
protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() {
return defaultGlobalFactoryBuilder(super.createGlobalObjectMapperFactoryBuilder());
// populate the instance the early load created, so it isn't replaced
return globalObjectMapperFactoryBuilder(super.createGlobalObjectMapperFactoryBuilder(), PaperConfigurationsInitializer.earlyGlobalConfig());
}
private static ObjectMapper.Factory.Builder defaultGlobalFactoryBuilder(ObjectMapper.Factory.Builder builder) {
return builder.addDiscoverer(InnerClassFieldDiscoverer.globalConfig(defaultFieldProcessors()));
private static ObjectMapper.Factory.Builder globalObjectMapperFactoryBuilder(final ObjectMapper.Factory.Builder builder, final GlobalConfiguration instance) {
return builder.addDiscoverer(InnerClassFieldDiscoverer.globalConfig(instance, defaultFieldProcessors()));
}
private static ObjectMapper.Factory.Builder earlyGlobalObjectMapperFactoryBuilder(final GlobalConfiguration instance) {
// not based on the default builder, it always adds the @PostProcess post-processor
return addDefaultConstraints(ObjectMapper.emptyFactoryBuilder())
.defaultNamingScheme(NamingSchemes.LOWER_CASE_DASHED)
.addNodeResolver(NodeResolver.nodeKey())
.addNodeResolver(NodeResolver.keyFromSetting())
.addNodeResolver(NodeResolver.nodeFromParent())
.addDiscoverer(FieldDiscoverer.emptyConstructorObject())
.addDiscoverer(FieldDiscoverer.record())
.addDiscoverer(InnerClassFieldDiscoverer.earlyGlobalConfig(instance, defaultFieldProcessors())); // added last, so it's checked first
}
@Override
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(RegistryAccess registryAccess) {
return super.createGlobalLoaderBuilder(registryAccess)
.defaultOptions((options) -> defaultGlobalOptions(registryAccess, options));
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(final ContextMap contextMap) {
final RegistryAccess access = contextMap.require(REGISTRY_ACCESS);
return super.createGlobalLoaderBuilder(contextMap)
.defaultOptions((options) -> defaultGlobalOptions(options, access));
}
private static ConfigurationOptions defaultGlobalOptions(RegistryAccess registryAccess, ConfigurationOptions options) {
private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options, RegistryAccess registryAccess) {
return options
.header(GLOBAL_HEADER)
.serializers(builder -> builder
.register(new ServerboundPacketClassSerializer())
.register(new RegistryValueSerializer<>(new TypeToken<DataComponentType<?>>() {}, registryAccess, Registries.DATA_COMPONENT_TYPE, false))
// stats.forced-custom-stat-values
.register(new TypeToken<Object2IntMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Object2IntMap<?>>(Object2IntOpenHashMap::new, Integer.TYPE))
.register(new RegistryHolderSerializer<>(new TypeToken<Identifier>() {}, registryAccess, Registries.CUSTOM_STAT, false))
// attributes.overrides
.register(new RegistryHolderSerializer<>(new TypeToken<Attribute>() {}, registryAccess, Registries.ATTRIBUTE, false))
// spawning.spawn-limits / spawning.ticks-per-spawn
.register(new TypeToken<Reference2IntMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2IntMap<?>>(Reference2IntOpenHashMap::new, Integer.TYPE))
.register(StringRepresentableSerializer::isValidFor, new StringRepresentableSerializer())
.register(new EnumValueSerializer())
);
}
@Override
public GlobalConfiguration initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException {
GlobalConfiguration configuration = super.initializeGlobalConfiguration(registryAccess);
public GlobalConfiguration initializeGlobalConfiguration(final DedicatedServer server) throws ConfigurateException {
GlobalConfiguration configuration = super.initializeGlobalConfiguration(server);
GlobalConfiguration.set(configuration);
return configuration;
}
@Override
protected ContextMap.Builder createDefaultContextMap(final RegistryAccess registryAccess) {
return super.createDefaultContextMap(registryAccess)
.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(createWorldConfigInstance(contextMap), defaultFieldProcessors()));
}
private static WorldConfiguration createWorldConfigInstance(ContextMap contextMap) {
return new WorldConfiguration(
contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(),
contextMap.require(Configurations.WORLD_KEY)
);
return new WorldConfiguration(contextMap.require(Configurations.WORLD_KEY));
}
@Override
@@ -262,7 +260,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
.register(DespawnRange.class, DespawnRange.SERIALIZER)
.register(StringRepresentableSerializer::isValidFor, new StringRepresentableSerializer())
.register(EngineMode.SERIALIZER)
.register(FallbackValueSerializer.create(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), MinecraftServer::getServer))
.register(FallbackValueSerializer.create(createWorldConfigInstance(contextMap), MinecraftServer::getServer))
.register(new RegistryValueSerializer<>(new TypeToken<EntityType<?>>() {}, access, Registries.ENTITY_TYPE, true))
.register(new RegistryValueSerializer<>(Item.class, access, Registries.ITEM, true))
.register(new RegistryValueSerializer<>(Block.class, access, Registries.BLOCK, true))
@@ -282,12 +280,48 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder();
V29_ZeroWorldHeight.apply(versionedBuilder);
V30_RenameFilterNbtFromSpawnEgg.apply(versionedBuilder);
V32_WorldConfigCleanup.apply(versionedBuilder);
// ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE
versionedBuilder.build().apply(node);
}
@Override
protected void applyGlobalConfigTransformations(ConfigurationNode node) throws ConfigurateException {
protected void applyGlobalConfigTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException {
applyGlobalTransformations(contextMap, node);
}
/**
* Applies removals and versioned transformations for the global config.
*
* @param contextMap context for the transformations, which must contain everything each of them requires
* @param node configuration node
*/
static void applyGlobalTransformations(final ContextMap contextMap, final ConfigurationNode node) throws ConfigurateException {
applyGlobalTransformations(contextMap, node, false);
}
/**
* Applies the global transformations that can run before the server exists, for reading {@link LoadEarly} fields.
* Every transformation requiring context that isn't available yet is skipped, so the node must never be saved.
*
* @param node configuration node
* @see PaperConfigurationsInitializer#loadEarlyGlobalConfig(Path)
*/
static void applyEarlyGlobalTransformations(final ConfigurationNode node) throws ConfigurateException {
applyGlobalTransformations(ContextMap.builder().build(), node, true);
}
private static List<ContextualTransformation> versionedGlobalTransformations() {
return List.of(
V29_LogIPs.INSTANCE,
V30_PacketIds.INSTANCE,
V31_AllowNetherPropertiesToConfig.INSTANCE,
V32_SpigotConfigCleanup.INSTANCE
// ADD FUTURE VERSIONED TRANSFORMS HERE
);
}
private static void applyGlobalTransformations(final ContextMap contextMap, final ConfigurationNode node, final boolean skipUnavailable) throws ConfigurateException {
ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
for (NodePath path : RemovedConfigurations.REMOVED_GLOBAL_PATHS) {
builder.addAction(path, TransformAction.remove());
@@ -295,22 +329,38 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
builder.build().apply(node);
final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder();
V29_LogIPs.apply(versionedBuilder);
V30_PacketIds.apply(versionedBuilder);
V31_AllowNetherPropertiesToConfig.apply(versionedBuilder);
// ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE
for (final ContextualTransformation transformation : versionedGlobalTransformations()) {
final List<String> missing = transformation.requiredContext().stream()
.filter(key -> !contextMap.has(key))
.map(ContextKey::name)
.toList();
if (missing.isEmpty()) {
versionedBuilder.addVersion(transformation.version(), transformation.create(contextMap));
} else if (!skipUnavailable) {
// the version is bumped and saved regardless, so the skipped migration would be lost for good
throw new IllegalStateException("Global config transformation to version " + transformation.version() + " requires " + String.join(", ", missing));
}
}
versionedBuilder.build().apply(node);
}
private static final List<Transformations.DefaultsAware> DEFAULT_AWARE_TRANSFORMATIONS = List.of(
FeatureSeedsGeneration::apply
);
/**
* Options for loading the global config file before {@code Bootstrap} has run and {@code RegistryAccess} exists.
* Only {@link LoadEarly} fields are deserialized, into the given instance, and no post-processors are run.
*
* @param options base options
* @param instance global config instance to populate
* @return early global config options
*/
static ConfigurationOptions earlyGlobalOptions(final ConfigurationOptions options, final GlobalConfiguration instance) {
return applyObjectMapperFactory(defaultOptions(options), PaperConfigurations::isConfigPart, earlyGlobalObjectMapperFactoryBuilder(instance).build());
}
@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));
FeatureSeedsGeneration.apply(builder, contextMap, defaultsNode);
builder.build().apply(worldNode);
}
@@ -326,17 +376,21 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
@Override
protected boolean isConfigType(final Type type) {
return isConfigPart(type);
}
private static boolean isConfigPart(final Type type) {
return ConfigurationPart.class.isAssignableFrom(erase(type));
}
public void reloadConfigs(MinecraftServer server) {
public void reloadConfigs(final DedicatedServer server) {
try {
this.initializeGlobalConfiguration(server.registryAccess(), reloader(this.globalConfigClass, GlobalConfiguration.get()));
this.initializeGlobalConfiguration(this.createGlobalContextMap(server).build(), reloader(this.globalConfigClass, GlobalConfiguration.get()));
this.initializeWorldDefaultsConfiguration(server.registryAccess());
for (ServerLevel level : server.getAllLevels()) {
for (final ServerLevel level : server.getAllLevels()) {
this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig()));
}
} catch (Exception ex) {
} catch (final Exception ex) {
throw new RuntimeException("Could not reload paper configuration files", ex);
}
}
@@ -348,51 +402,19 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
}
private static ContextMap createWorldContextMap(ServerLevel level) {
return createWorldContextMap(level.getServer().storageSource.getDimensionPath(level.dimension()), level.dimension().identifier(), level.spigotConfig, level.registryAccess(), level.getGameRules());
return createWorldContextMap(level.getServer().storageSource.getDimensionPath(level.dimension()), level.dimension().identifier(), level.registryAccess(), level.getGameRules());
}
public static ContextMap createWorldContextMap(final Path dir, final Identifier worldKey, final SpigotWorldConfig spigotConfig, final RegistryAccess registryAccess, final GameRules gameRules) {
public static ContextMap createWorldContextMap(final Path dir, final Identifier worldKey, final RegistryAccess registryAccess, final GameRules gameRules) {
return ContextMap.builder()
.put(WORLD_DIRECTORY, dir)
.put(WORLD_KEY, worldKey)
.put(SPIGOT_WORLD_CONFIG_CONTEXT_KEY, Suppliers.ofInstance(spigotConfig))
.put(REGISTRY_ACCESS, registryAccess)
.put(GAME_RULES, gameRules)
.build();
}
public static PaperConfigurations setup(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception {
final Path legacy = Files.isSymbolicLink(legacyConfig) ? Files.readSymbolicLink(legacyConfig) : legacyConfig;
if (needsConverting(legacyConfig)) {
final String legacyFileName = legacyConfig.getFileName().toString();
try {
if (Files.exists(configDir) && !Files.isDirectory(configDir)) {
throw new RuntimeException("Paper needs to create a '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + configDir.toAbsolutePath() + "'. Please remove it and restart the server.");
}
final Path backupDir = configDir.resolve(BACKUP_DIR);
if (Files.exists(backupDir) && !Files.isDirectory(backupDir)) {
throw new RuntimeException("Paper needs to create a '" + BACKUP_DIR + "' directory in the '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + BACKUP_DIR + "'. Please remove it and restart the server.");
}
createDirectoriesSymlinkAware(backupDir);
final String backupFileName = legacyFileName + ".old";
final Path legacyConfigBackup = backupDir.resolve(backupFileName);
if (Files.exists(legacyConfigBackup) && !Files.isRegularFile(legacyConfigBackup)) {
throw new RuntimeException("Paper needs to create a '" + backupFileName + "' file in the '" + backupDir.toAbsolutePath() + "' folder. You already have a non-file named '" + backupFileName + "'. Please remove it and restart the server.");
}
Files.move(legacyConfig.toRealPath(), legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup
if (Files.isSymbolicLink(legacyConfig)) {
Files.delete(legacyConfig);
}
final Path replacementFile = legacy.resolveSibling(legacyFileName + "-README.txt");
if (Files.notExists(replacementFile)) {
Files.createFile(replacementFile);
Files.writeString(replacementFile, String.format(MOVED_NOTICE, configDir.toAbsolutePath()));
}
convert(legacyConfigBackup, configDir, worldFolder, spigotConfig);
} catch (final IOException ex) {
throw new RuntimeException("Could not convert '" + legacyFileName + "' to the new configuration format", ex);
}
}
public static PaperConfigurations setup(final Path configDir) throws Exception {
try {
createDirectoriesSymlinkAware(configDir);
return new PaperConfigurations(configDir);
@@ -401,88 +423,41 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
}
}
private static void convert(final Path legacyConfig, final Path configDir, final Path worldFolder, final File spigotConfig) throws Exception {
createDirectoriesSymlinkAware(configDir);
final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig);
final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME));
final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME));
final ConfigurationNode legacy = legacyLoader.load();
checkState(!legacy.virtual(), "can't be virtual");
final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt();
final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy();
checkState(!legacyWorldSettings.virtual(), "can't be virtual");
legacy.removeChild("world-settings");
// Apply legacy transformations before settings flatten
final YamlConfiguration spigotConfiguration = loadLegacyConfigFile(spigotConfig); // needs to change spigot config values in this transformation
LegacyPaperConfig.transformation(spigotConfiguration).apply(legacy);
spigotConfiguration.save(spigotConfig);
legacy.mergeFrom(legacy.node("settings")); // flatten "settings" to root
legacy.removeChild("settings");
LegacyPaperConfig.toNewFormat().apply(legacy);
globalLoader.save(legacy); // save converted node to new global location
final ConfigurationNode worldDefaults = legacyWorldSettings.node("default").copy();
checkState(!worldDefaults.virtual());
worldDefaults.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
legacyWorldSettings.removeChild("default");
LegacyPaperWorldConfig.transformation().apply(worldDefaults);
LegacyPaperWorldConfig.toNewFormat().apply(worldDefaults);
worldDefaultsLoader.save(worldDefaults);
legacyWorldSettings.childrenMap().forEach((world, legacyWorldNode) -> {
try {
legacyWorldNode.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
LegacyPaperWorldConfig.transformation().apply(legacyWorldNode);
LegacyPaperWorldConfig.toNewFormat().apply(legacyWorldNode);
ConfigurationLoaders.naturallySortedWithoutHeader(worldFolder.resolve(world.toString()).resolve(WORLD_CONFIG_FILE_NAME)).save(legacyWorldNode); // save converted node to new location
} catch (final ConfigurateException ex) {
ex.printStackTrace();
}
});
}
private static boolean needsConverting(final Path legacyConfig) {
return Files.exists(legacyConfig) && Files.isRegularFile(legacyConfig);
}
@Deprecated
public YamlConfiguration createLegacyObject(final MinecraftServer server) {
YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile());
ConfigurationSection worlds = global.createSection("__________WORLDS__________");
final YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile());
final 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()));
for (final ServerLevel level : server.getAllLevels()) {
worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(this.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;
/**
* Creates the global config with all defaults, the same way {@link #initializeGlobalConfiguration(DedicatedServer)} would.
*
* @param registryAccess registry access
* @return default global config
*/
@VisibleForTesting
static GlobalConfiguration createGlobalForTesting(final RegistryAccess registryAccess) throws SerializationException {
final GlobalConfiguration instance = new GlobalConfiguration();
final ObjectMapper.Factory factory = globalObjectMapperFactoryBuilder(defaultObjectMapperFactoryBuilder(), instance).build();
final ConfigurationOptions options = applyObjectMapperFactory(defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults()), registryAccess), PaperConfigurations::isConfigPart, factory);
BasicConfigurationNode.root(options).require(GlobalConfiguration.class); // populates the instance passed to the factory
return instance;
}
@VisibleForTesting
static ConfigurationNode createForTesting(RegistryAccess registryAccess) {
ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build();
ConfigurationOptions options = defaultGlobalOptions(registryAccess, defaultOptions(ConfigurationOptions.defaults()))
.serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer()));
return BasicConfigurationNode.root(options);
public static WorldConfiguration createWorldDefaultsForTesting() {
return new WorldConfiguration(Configurations.WORLD_DEFAULTS_KEY);
}
// Symlinks are not correctly checked in createDirectories
static void createDirectoriesSymlinkAware(Path path) throws IOException {
static void createDirectoriesSymlinkAware(final Path path) throws IOException {
if (!Files.isDirectory(path)) {
Files.createDirectories(path);
}
@@ -0,0 +1,396 @@
package io.papermc.paper.configuration;
import com.mojang.logging.LogUtils;
import io.papermc.paper.configuration.mapping.LoadEarly;
import io.papermc.paper.configuration.transformation.global.LegacyBukkitConfig;
import io.papermc.paper.configuration.transformation.global.LegacyPaperConfig;
import io.papermc.paper.configuration.transformation.global.LegacySpigotConfig;
import io.papermc.paper.configuration.transformation.world.LegacyPaperWorldConfig;
import io.papermc.paper.configuration.transformation.world.LegacySpigotWorldConfig;
import io.papermc.paper.configuration.yaml.YamlConfigurationLoader;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Set;
import joptsimple.OptionSet;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.spongepowered.configurate.BasicConfigurationNode;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.serialize.SerializationException;
import static com.google.common.base.Preconditions.checkState;
import static io.papermc.paper.configuration.PaperConfigurations.GLOBAL_CONFIG_FILE_NAME;
import static io.papermc.paper.configuration.PaperConfigurations.WORLD_CONFIG_FILE_NAME;
import static io.papermc.paper.configuration.PaperConfigurations.WORLD_DEFAULTS_CONFIG_FILE_NAME;
import static io.papermc.paper.configuration.PaperConfigurations.createDirectoriesSymlinkAware;
public final class PaperConfigurationsInitializer {
private static final Logger LOGGER = LogUtils.getClassLogger();
private static final String BACKUP_DIR ="legacy-backup";
private static final String MOVED_NOTICE = """
The global and world default configuration files have moved to %s
and the world-specific configuration file has been moved inside
the respective world folder.
See https://docs.papermc.io/paper/configuration for more information.
""";
// Also read from contexts that never actually boot a server
private static @Nullable GlobalConfiguration earlyGlobalConfig;
/**
* Returns the global config to read {@link LoadEarly} fields from, at any point during startup.
* <p>
* Until {@code DedicatedServer#initServer} fully loads the {@link GlobalConfiguration}, this returns the
* instance created by {@link #load(OptionSet)}, which only has its {@link LoadEarly} fields populated.
* Every other field holds its default or is {@code null}. The full load then populates this same instance.
*
* @return global config with at least its early fields loaded
*/
public static GlobalConfiguration earlyGlobalConfig() {
final GlobalConfiguration global = GlobalConfiguration.get();
//noinspection ConstantValue
if (global != null) {
return global;
}
if (earlyGlobalConfig == null) {
try {
earlyGlobalConfig = loadEarlyGlobalConfig(null);
} catch (final ConfigurateException ex) {
throw new RuntimeException("Could not create the default early global config", ex);
}
}
return earlyGlobalConfig;
}
/**
* Runs the early configuration load for the {@link #earlyGlobalConfig()}.
*
* @param options startup options
*/
public static void load(final OptionSet options) throws IOException {
final Path configDirPath = ((File) options.valueOf("paper-settings-directory")).toPath();
final GlobalConfiguration configuration = loadEarlyGlobalConfig(configDirPath.resolve(GLOBAL_CONFIG_FILE_NAME));
// Migration only comes later, so we need to manually look into legacy files that haven't been converted yet
final Path legacyBukkitConfigPath = ((File) options.valueOf("bukkit-settings")).toPath();
if (needsConverting(legacyBukkitConfigPath)) {
final YamlConfiguration bukkit = loadLegacyConfigFile(legacyBukkitConfigPath);
configuration.misc.worldContainer = bukkit.getString("settings.world-container", configuration.misc.worldContainer);
configuration.misc.updateFolder = bukkit.getString("settings.update-folder", configuration.misc.updateFolder);
}
final Path legacySpigotConfigPath = ((File) options.valueOf("spigot-settings")).toPath();
if (needsConverting(legacySpigotConfigPath)) {
final YamlConfiguration spigot = loadLegacyConfigFile(legacySpigotConfigPath);
if (spigot.contains("advancements.disabled")) {
configuration.advancements.disabled = Set.copyOf(spigot.getStringList("advancements.disabled"));
}
}
earlyGlobalConfig = configuration;
}
static GlobalConfiguration loadEarlyGlobalConfig(final @Nullable Path globalConfigFile) throws ConfigurateException {
final GlobalConfiguration configuration = new GlobalConfiguration();
final ConfigurationNode node;
if (globalConfigFile == null || Files.notExists(globalConfigFile)) {
node = BasicConfigurationNode.root(PaperConfigurations.earlyGlobalOptions(ConfigurationOptions.defaults(), configuration));
} else {
node = ConfigurationLoaders.naturallySorted()
.defaultOptions(options -> PaperConfigurations.earlyGlobalOptions(options, configuration))
.path(globalConfigFile)
.build()
.load();
final ConfigurationNode version = node.node(Configuration.VERSION_FIELD);
if (version.virtual()) { // See Configurations#verifyGlobalConfigVersion
version.raw(GlobalConfiguration.CURRENT_VERSION);
}
PaperConfigurations.applyEarlyGlobalTransformations(node);
}
node.require(GlobalConfiguration.class); // populates the instance passed to the options
return configuration;
}
public static PaperConfigurations create(final OptionSet options) throws Exception {
final Path legacyBukkitConfigPath = ((File) options.valueOf("bukkit-settings")).toPath();
final Path legacySpigotConfigPath = ((File) options.valueOf("spigot-settings")).toPath();
final Path legacyPaperConfig = ((File) options.valueOf("paper-settings")).toPath();
final Path configDirPath = ((File) options.valueOf("paper-settings-directory")).toPath();
final Path worldFolder = options.has("universe") ? ((File) options.valueOf("universe")).toPath() : Path.of(earlyGlobalConfig().misc.worldContainer); // the launch argument always wins
convertLegacy(legacyPaperConfig, configDirPath, worldFolder, legacySpigotConfigPath, legacyBukkitConfigPath, worldFolder.resolve(levelName(options)));
return PaperConfigurations.setup(configDirPath);
}
private static void convertLegacy(final Path legacyPaperConfig, final Path configDir, final Path worldFolder, final Path legacySpigotConfig, final Path legacyBukkitConfig, final Path levelFolder) throws Exception {
if (needsConverting(legacyPaperConfig)) {
final String legacyFileName = legacyPaperConfig.getFileName().toString();
try {
final Path legacyConfigBackup = makeConfigFileBackup(legacyPaperConfig, configDir);
convertLegacyPaperConfig(legacyConfigBackup, configDir, worldFolder, legacySpigotConfig);
} catch (final IOException ex) {
throw new RuntimeException("Could not convert '" + legacyFileName + "' to the new configuration format", ex);
}
}
if (needsConverting(legacyBukkitConfig)) {
try {
final Path legacyConfigBackup = makeConfigFileBackup(legacyBukkitConfig, configDir);
convertLegacyBukkitConfig(legacyConfigBackup, configDir);
} catch (final IOException ex) {
throw new RuntimeException("Could not convert '" + legacyBukkitConfig.getFileName() + "' to the new configuration format", ex);
}
}
if (needsConverting(legacySpigotConfig)) {
final String legacyFileName = legacySpigotConfig.getFileName().toString();
try {
final Path legacyConfigBackup = makeConfigFileBackup(legacySpigotConfig, configDir);
convertLegacySpigotConfig(legacyConfigBackup, configDir, levelFolder);
} catch (final IOException ex) {
throw new RuntimeException("Could not convert '" + legacyFileName + "' to the new configuration format", ex);
}
}
}
private static Path makeConfigFileBackup(final Path legacyConfigPath, final Path configDir) throws IOException {
if (Files.exists(configDir) && !Files.isDirectory(configDir)) {
throw new RuntimeException("Paper needs to create a '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + configDir.toAbsolutePath() + "'. Please remove it and restart the server.");
}
final Path backupDir = configDir.resolve(BACKUP_DIR);
if (Files.exists(backupDir) && !Files.isDirectory(backupDir)) {
throw new RuntimeException("Paper needs to create a '" + BACKUP_DIR + "' directory in the '" + configDir.toAbsolutePath() + "' folder. You already have a non-directory named '" + BACKUP_DIR + "'. Please remove it and restart the server.");
}
createDirectoriesSymlinkAware(backupDir);
final Path legacyFileName = legacyConfigPath.getFileName();
final String backupFileName = legacyFileName + ".old";
final Path legacyConfigBackup = backupDir.resolve(backupFileName);
if (Files.exists(legacyConfigBackup) && !Files.isRegularFile(legacyConfigBackup)) {
throw new RuntimeException("Paper needs to create a '" + backupFileName + "' file in the '" + backupDir.toAbsolutePath() + "' folder. You already have a non-file named '" + backupFileName + "'. Please remove it and restart the server.");
}
// resolve the symlink before the move, the link is gone by the time the README is placed
final boolean symlink = Files.isSymbolicLink(legacyConfigPath);
final Path realLegacyPath = symlink ? Files.readSymbolicLink(legacyConfigPath) : legacyConfigPath;
Files.move(legacyConfigPath.toRealPath(), legacyConfigBackup, StandardCopyOption.REPLACE_EXISTING); // make backup
if (symlink) {
Files.delete(legacyConfigPath);
}
final Path replacementFile = realLegacyPath.resolveSibling(legacyFileName + "-README.txt");
if (Files.notExists(replacementFile)) {
Files.createFile(replacementFile);
Files.writeString(replacementFile, String.format(MOVED_NOTICE, configDir.toAbsolutePath()));
}
return legacyConfigBackup;
}
private static void convertLegacyBukkitConfig(final Path legacyConfig, final Path configDir) throws Exception {
createDirectoriesSymlinkAware(configDir);
final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME));
final ConfigurationNode global = globalLoader.load();
final ConfigurationNode legacy = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig).load();
// spawn limits, spawn intervals and the save interval are world settings now
final ConfigurationNode worldBound = legacy.copy();
LegacyBukkitConfig.migrateWorldDefaults(worldBound);
if (!worldBound.empty()) {
final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME));
final ConfigurationNode worldDefaults = worldDefaultsLoader.load();
overwriteDeferredValues(worldBound, worldDefaults);
worldDefaultsLoader.save(worldDefaults);
}
LegacyBukkitConfig.migrate(legacy);
global.mergeFrom(legacy);
globalLoader.save(global);
LOGGER.info("Converted bukkit.yml into {}", GLOBAL_CONFIG_FILE_NAME);
}
private static void convertLegacyPaperConfig(final Path legacyConfig, final Path configDir, final Path worldFolder, final Path spigotConfig) throws Exception {
createDirectoriesSymlinkAware(configDir);
final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig);
final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME));
final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME));
final ConfigurationNode legacy = legacyLoader.load();
checkState(!legacy.virtual(), "can't be virtual");
final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt();
final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy();
checkState(!legacyWorldSettings.virtual(), "can't be virtual");
legacy.removeChild("world-settings");
// Apply legacy transformations before settings flatten
final YamlConfiguration spigotConfiguration = loadLegacyConfigFile(spigotConfig); // needs to change spigot config values in this transformation
LegacyPaperConfig.transformation(spigotConfiguration).apply(legacy);
spigotConfiguration.save(spigotConfig.toFile());
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) {
LOGGER.error("Failed to convert legacy world config", ex);
}
});
}
private static void convertLegacySpigotConfig(final Path legacyConfig, final Path configDir, final Path levelFolder) throws Exception {
createDirectoriesSymlinkAware(configDir);
final YamlConfigurationLoader legacyLoader = ConfigurationLoaders.naturallySortedWithoutHeader(legacyConfig);
final YamlConfigurationLoader globalLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(GLOBAL_CONFIG_FILE_NAME));
final YamlConfigurationLoader worldDefaultsLoader = ConfigurationLoaders.naturallySortedWithoutHeader(configDir.resolve(WORLD_DEFAULTS_CONFIG_FILE_NAME));
final ConfigurationNode legacy = legacyLoader.load();
checkState(!legacy.virtual(), "can't be virtual");
final int version = legacy.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).getInt();
final ConfigurationNode legacyWorldSettings = legacy.node("world-settings").copy();
legacy.removeChild("world-settings");
// Apply legacy transformations new format change
LegacySpigotConfig.migrate(legacy);
final ConfigurationNode global = globalLoader.load();
global.mergeFrom(legacy); // merge converted spigot global into main global
liftBelowZeroGeneration(legacyWorldSettings, global);
globalLoader.save(global); // save converted node to new global location
if (legacyWorldSettings.virtual()) {
return;
}
final ConfigurationNode worldDefaults = legacyWorldSettings.node("default").copy();
legacyWorldSettings.removeChild("default");
if (!worldDefaults.virtual()) {
worldDefaults.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
LegacySpigotWorldConfig.migrate(worldDefaults, "spigot.yml world-settings.default");
final ConfigurationNode defaults = worldDefaultsLoader.load();
defaults.mergeFrom(worldDefaults);
worldDefaultsLoader.save(defaults);
}
legacyWorldSettings.childrenMap().forEach((world, legacyWorldNode) -> {
final String key = world.toString();
try {
final Path worldConfigDir = resolveWorldConfigDirectory(levelFolder, key);
legacyWorldNode.node(Configuration.LEGACY_CONFIG_VERSION_FIELD).raw(version);
LegacySpigotWorldConfig.migrate(legacyWorldNode, "spigot.yml world-settings." + key);
final YamlConfigurationLoader worldLoader = ConfigurationLoaders.naturallySortedWithoutHeader(worldConfigDir.resolve(WORLD_CONFIG_FILE_NAME));
final ConfigurationNode worldNode = worldLoader.load();
worldNode.mergeFrom(legacyWorldNode);
createDirectoriesSymlinkAware(worldConfigDir);
worldLoader.save(worldNode);
} catch (final Exception ex) {
LOGGER.error("Failed to convert the spigot.yml world settings for '{}'. They have been left in {} for you to move over by hand.", key, legacyConfig, ex);
}
});
}
private static void liftBelowZeroGeneration(final ConfigurationNode legacyWorldSettings, final ConfigurationNode global) throws ConfigurateException {
if (legacyWorldSettings.virtual()) {
return;
}
// Moved into the global config
final String key = "below-zero-generation-in-existing-chunks";
final ConfigurationNode target = global.node("misc", key);
final ConfigurationNode fromDefault = legacyWorldSettings.node("default", key);
if (!fromDefault.virtual() && target.virtual()) {
target.set(fromDefault.raw());
}
for (final java.util.Map.Entry<Object, ? extends ConfigurationNode> entry : legacyWorldSettings.childrenMap().entrySet()) {
entry.getValue().removeChild(key);
}
}
private static Path resolveWorldConfigDirectory(final Path levelFolder, final String worldKey) {
final int separator = worldKey.indexOf(':');
if (separator < 0) {
return levelFolder.resolveSibling(worldKey);
}
return levelFolder.resolve("dimensions").resolve(worldKey.substring(0, separator)).resolve(worldKey.substring(separator + 1));
}
private static String levelName(final OptionSet options) {
final Object fromArgs = options.valueOf("world");
if (fromArgs instanceof final String name && !name.isEmpty()) {
return name;
}
final Path propertiesFile = ((File) options.valueOf("config")).toPath();
if (Files.isRegularFile(propertiesFile)) {
final java.util.Properties properties = new java.util.Properties();
try (final java.io.Reader reader = Files.newBufferedReader(propertiesFile, StandardCharsets.UTF_8)) {
properties.load(reader);
} catch (final IOException ex) {
LOGGER.warn("Could not read {} while locating world config files, assuming the default level name", propertiesFile, ex);
}
return properties.getProperty("level-name", "world");
}
return "world";
}
/**
* Copies every leaf of {@code source} into {@code target}, replacing values that are still the
* sentinel meaning "defer to bukkit.yml" ({@code -1}, or {@code default}). A world value the admin
* had already set to something real keeps winning, as it did before.
*/
private static void overwriteDeferredValues(final ConfigurationNode source, final ConfigurationNode target) throws SerializationException {
if (source.isMap()) {
for (final java.util.Map.Entry<Object, ? extends ConfigurationNode> entry : source.childrenMap().entrySet()) {
overwriteDeferredValues(entry.getValue(), target.node(entry.getKey()));
}
return;
}
if (target.virtual() || target.getInt(0) < 0 || "default".equals(target.getString())) {
target.set(source.raw());
}
}
private static boolean needsConverting(final Path legacyConfig) {
return Files.exists(legacyConfig) && Files.isRegularFile(legacyConfig);
}
private static YamlConfiguration loadLegacyConfigFile(final Path configFile) throws IOException {
final YamlConfiguration config = new YamlConfiguration();
if (Files.exists(configFile)) {
try {
config.load(Files.newBufferedReader(configFile, StandardCharsets.UTF_8));
} catch (final Exception ex) {
throw new IOException("Failed to load configuration file: " + configFile.toAbsolutePath(), ex);
}
}
return config;
}
private PaperConfigurationsInitializer() {}
}
@@ -1,6 +1,5 @@
package io.papermc.paper.configuration;
import org.spigotmc.SpigotConfig;
public class PaperServerConfiguration implements ServerConfiguration {
@@ -11,6 +10,6 @@ public class PaperServerConfiguration implements ServerConfiguration {
@Override
public boolean isProxyEnabled() {
return GlobalConfiguration.get().proxies.velocity.enabled || SpigotConfig.bungee;
return GlobalConfiguration.get().proxies.velocity.enabled || io.papermc.paper.configuration.GlobalConfiguration.get().proxies.bungeeCord.enabled;
}
}
@@ -85,11 +85,12 @@ interface RemovedConfigurations {
path("commands", "fix-target-selector-tag-completion"),
path("misc", "fix-entity-position-desync"),
path("chunk-system", "gen-parallelism"),
path("logging"), // server is no longer obfuscated since 26.1
path("logging", "deobfuscate-stacktraces"), // server is no longer obfuscated since 26.1
path("unsupported-settings", "compression-format"),
path("commands", "time-command-affects-all-worlds"), // replaced with time.affects-all-worlds
path("misc", "fix-far-end-terrain-generation"),
path("misc", "send-full-pos-for-item-entities")
path("misc", "send-full-pos-for-item-entities"),
path("timings"),
};
}
@@ -4,8 +4,6 @@ import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.mojang.logging.LogUtils;
import io.papermc.paper.FeatureHooks;
import io.papermc.paper.configuration.legacy.MaxEntityCollisionsInitializer;
import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization;
import io.papermc.paper.configuration.mapping.MergeMap;
import io.papermc.paper.configuration.serializer.NbtPathSerializer;
import io.papermc.paper.configuration.serializer.collection.map.ThrowExceptions;
@@ -16,10 +14,10 @@ import io.papermc.paper.configuration.type.Duration;
import io.papermc.paper.configuration.type.DurationOrDisabled;
import io.papermc.paper.configuration.type.EngineMode;
import io.papermc.paper.configuration.type.fallback.ArrowDespawnRate;
import io.papermc.paper.configuration.type.fallback.AutosavePeriod;
import io.papermc.paper.configuration.type.number.BelowZeroToEmpty;
import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import io.papermc.paper.configuration.type.number.LongOr;
import it.unimi.dsi.fastutil.objects.Reference2IntMap;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2LongMap;
@@ -60,7 +58,6 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.feature.Feature;
import org.slf4j.Logger;
import org.spigotmc.SpigotWorldConfig;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.PostProcess;
@@ -71,13 +68,11 @@ import org.spongepowered.configurate.serialize.SerializationException;
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
public class WorldConfiguration extends ConfigurationPart {
private static final Logger LOGGER = LogUtils.getClassLogger();
static final int CURRENT_VERSION = 31; // (when you change the version, change the comment, so it conflicts on rebases): migrate spawn loaded configs to gamerule
static final int CURRENT_VERSION = 32; // (when you change the version, change the comment, so it conflicts on rebases): merge configs
private final transient SpigotWorldConfig spigotConfig;
private final transient Identifier worldKey;
WorldConfiguration(final SpigotWorldConfig spigotConfig, final Identifier worldKey) {
this.spigotConfig = spigotConfig;
WorldConfiguration(final Identifier worldKey) {
this.worldKey = worldKey;
}
@@ -170,8 +165,35 @@ public class WorldConfiguration extends ConfigurationPart {
public Spawning spawning;
public class Spawning extends ConfigurationPart {
public ArrowDespawnRate nonPlayerArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig);
public ArrowDespawnRate creativeArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this.spigotConfig);
@Comment("The radius around a player, in chunks, that mobs may naturally spawn in.")
public int mobSpawnRange = 8;
@Comment("Mobs spawned by monster spawners.")
public SpawnerMobs spawnerMobs;
public class SpawnerMobs extends ConfigurationPart {
@Comment("Spawn them without AI.")
public boolean nerf = false;
@Comment("Whether nerfed mobs still jump and float in water.")
public boolean nerfedShouldJump = false;
}
@Comment("Zombified piglins spawned by nether portals.")
public PortalZombifiedPiglins portalZombifiedPiglins;
public class PortalZombifiedPiglins extends ConfigurationPart {
public boolean enabled = true;
@Comment("Spawn them without AI.")
public boolean nerf = false;
}
@Comment("Ticks before a dropped item despawns.")
public int itemDespawnRate = 6000;
@Comment("Ticks before a stuck arrow despawns.")
public int arrowDespawnRate = 1200;
@Comment("Ticks before a stuck trident despawns. Defaults to the arrow despawn rate.")
public IntOr.Default tridentDespawnRate = IntOr.Default.USE_DEFAULT;
public ArrowDespawnRate nonPlayerArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this);
public ArrowDespawnRate creativeArrowDespawnRate = ArrowDespawnRate.def(WorldConfiguration.this);
public IntOr.Disabled maxArrowDespawnInvulnerability = new IntOr.Disabled(OptionalInt.of(200));
public boolean filterBadTileEntityNbtFromFallingBlocks = true;
public List<NbtPathArgument.NbtPath> filteredEntityTagNbtPaths = NbtPathSerializer.fromString(List.of("Pos", "Motion", "sleeping_pos"));
@@ -179,12 +201,28 @@ public class WorldConfiguration extends ConfigurationPart {
public boolean perPlayerMobSpawns = true;
public boolean scanForLegacyEnderDragon = true;
@MergeMap
public Reference2IntMap<MobCategory> spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1)));
public Reference2IntMap<MobCategory> spawnLimits = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> {
map.put(MobCategory.MONSTER, 70);
map.put(MobCategory.CREATURE, 10);
map.put(MobCategory.WATER_CREATURE, 5);
map.put(MobCategory.WATER_AMBIENT, 20);
map.put(MobCategory.UNDERGROUND_WATER_CREATURE, 5);
map.put(MobCategory.AXOLOTLS, 5);
map.put(MobCategory.AMBIENT, 15);
});
@MergeMap
public Map<MobCategory, DespawnRangePair> despawnRanges = Arrays.stream(MobCategory.values()).collect(Collectors.toMap(Function.identity(), category -> DespawnRangePair.createDefault()));
public DespawnRange.Shape despawnRangeShape = DespawnRange.Shape.ELLIPSOID;
@MergeMap
public Reference2IntMap<MobCategory> ticksPerSpawn = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> Arrays.stream(NaturalSpawner.SPAWNING_CATEGORIES).forEach(mobCategory -> map.put(mobCategory, -1)));
public Reference2IntMap<MobCategory> ticksPerSpawn = Util.make(new Reference2IntOpenHashMap<>(NaturalSpawner.SPAWNING_CATEGORIES.length), map -> {
map.put(MobCategory.MONSTER, 1);
map.put(MobCategory.CREATURE, 400);
map.put(MobCategory.WATER_CREATURE, 1);
map.put(MobCategory.WATER_AMBIENT, 1);
map.put(MobCategory.UNDERGROUND_WATER_CREATURE, 1);
map.put(MobCategory.AXOLOTLS, 1);
map.put(MobCategory.AMBIENT, 1);
});
@ConfigSerializable
public record DespawnRangePair(@Required DespawnRange hard, @Required DespawnRange soft) {
@@ -275,7 +313,6 @@ public class WorldConfiguration extends ConfigurationPart {
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;
@@ -310,7 +347,6 @@ public class WorldConfiguration extends ConfigurationPart {
}
public boolean disablePlayerCrits = false;
public boolean nerfPigmenFromNetherPortals = false;
@Comment("Prevents merging items that are not on the same y level, preventing potential visual artifacts.")
public boolean onlyMergeItemsHorizontally = false;
public PillagerPatrols pillagerPatrols;
@@ -338,6 +374,67 @@ public class WorldConfiguration extends ConfigurationPart {
public IntOr.Disabled stuckEntityPoiRetryDelay = new IntOr.Disabled(OptionalInt.of(200));
@Comment("If specified, this setting controls how often to tick inactive mobs' goal selectors. Higher values reduce the performance impact of ticking mobs that are far away. Measured in ticks.")
public IntOr.Disabled inactiveMobGoalSelectorThrottleRate = IntOr.Disabled.DISABLED;
@Comment("Whether zombies are always hostile towards villagers, regardless of difficulty.")
public boolean zombieAggressiveTowardsVillager = true;
@Comment("The radius within which dropped items and experience orbs merge. Set to -1 to disable.")
public MergeRadius mergeRadius;
public class MergeRadius extends ConfigurationPart {
public double item = 0.5;
public double experience = -1.0;
}
}
@Comment("The horizontal distance, in blocks, at which entities of each category are sent to players.")
public TrackingRange trackingRange;
public class TrackingRange extends ConfigurationPart {
public int player = 128;
public int animal = 96;
public int monster = 96;
public int misc = 96;
public int display = 128;
public int other = 64;
}
@Comment("The distance, in blocks, at which entities of each category are ticked. Entities outside it are mostly frozen.")
public ActivationRange activationRange;
public class ActivationRange extends ConfigurationPart {
public int animals = 32;
public int monsters = 32;
public int raiders = 64;
public int misc = 16;
public int water = 16;
public int villagers = 32;
public int flyingMonsters = 32;
@Comment("Ticks a villager is immune to deactivation after starting work.")
public int villagersWorkImmunityAfter = 100;
public int villagersWorkImmunityFor = 20;
@Comment("Whether villagers stay active while panicking.")
public boolean villagersActiveForPanic = true;
public boolean tickInactiveVillagers = true;
@Comment("Whether spectators keep nearby entities active.")
public boolean ignoreSpectators = false;
@Comment("Periodically wakes a limited number of deactivated entities so they do not stay frozen forever.")
public WakeUpInactive wakeUpInactive;
public class WakeUpInactive extends ConfigurationPart {
public int animalsMaxPerTick = 4;
public int animalsEvery = 1200;
public int animalsFor = 100;
public int monstersMaxPerTick = 8;
public int monstersEvery = 400;
public int monstersFor = 100;
public int villagersMaxPerTick = 4;
public int villagersEvery = 600;
public int villagersFor = 100;
public int flyingMonstersMaxPerTick = 8;
public int flyingMonstersEvery = 200;
public int flyingMonstersFor = 100;
}
}
public TrackingRangeY trackingRangeY;
@@ -409,10 +506,87 @@ public class WorldConfiguration extends ConfigurationPart {
public boolean useVanillaWorldScoreboardNameColoring = false;
}
@Comment("""
Percentage chance that a growth tick actually advances the plant.
100 is vanilla speed, 50 is half as fast, 200 is twice as fast.""")
public GrowthModifiers growthModifiers;
public class GrowthModifiers extends ConfigurationPart {
public int cactus = 100;
public int cane = 100;
public int melon = 100;
public int mushroom = 100;
public int pumpkin = 100;
public int sapling = 100;
public int beetroot = 100;
public int carrot = 100;
public int potato = 100;
public int torchFlower = 100;
public int wheat = 100;
public int netherWart = 100;
public int vine = 100;
public int cocoa = 100;
public int bamboo = 100;
public int sweetBerry = 100;
public int kelp = 100;
public int twistingVines = 100;
public int weepingVines = 100;
public int caveVines = 100;
public int glowBerry = 100;
public int pitcherPlant = 100;
}
@Comment("Exhaustion added by each action. Higher values make the hunger bar drain faster.")
public Hunger hunger;
public class Hunger extends ConfigurationPart {
public float jumpWalkExhaustion = 0.05f;
public float jumpSprintExhaustion = 0.2f;
public float combatExhaustion = 0.1f;
public float regenExhaustion = 6.0f;
public float swimMultiplier = 0.01f;
public float sprintMultiplier = 0.1f;
public float otherMultiplier = 0.0f;
}
@Comment("""
Salts used when deciding where structures and other seeded features generate.
Changing these changes where they appear in newly generated chunks.""")
public Seeds seeds;
public class Seeds extends ConfigurationPart {
public int village = 10387312;
public int desert = 14357617;
public int igloo = 14357618;
public int jungle = 14357619;
public int swamp = 14357620;
public int monument = 10387313;
public int shipwreck = 165745295;
public int ocean = 14357621;
public int outpost = 165745296;
public int endCity = 10387313;
public int slime = 987234911;
public int nether = 30084232;
public int mansion = 10387319;
public int fossil = 14357921;
public int portal = 34222645;
public int ancientCity = 20083232;
public int trailRuins = 83469867;
public int trialChambers = 94251327;
public int abandonedCamp = 91231127;
public int buriedTreasure = 10387320;
@Comment("Set to 'default' to use the vanilla value.")
public IntOr.Default mineshaft = IntOr.Default.USE_DEFAULT;
@Comment("Set to 'default' to use the vanilla value.")
public LongOr.Default stronghold = LongOr.Default.USE_DEFAULT;
}
public Environment environment;
public class Environment extends ConfigurationPart {
public boolean disableThunder = false;
@Comment("The 1-in-N chance per tick of a thunderstorm starting. Higher means less frequent.")
public int thunderChance = 100000;
public boolean disableIceAndSnow = false;
public boolean optimizeExplosions = false;
public boolean disableExplosionKnockback = false;
@@ -468,6 +642,8 @@ public class WorldConfiguration extends ConfigurationPart {
public class Fixes extends ConfigurationPart {
public boolean fixItemsMergingThroughWalls = false;
@Comment("The maximum number of primed TNT entities ticked per tick. Set to -1 to disable the limit.")
public int maxTntPerTick = 100;
public boolean disableUnloadedChunkEnderpearlExploit = false;
public boolean preventTntFromMovingInWater = false;
public boolean splitOverstackedLoot = true;
@@ -492,6 +668,10 @@ public class WorldConfiguration extends ConfigurationPart {
public class Hopper extends ConfigurationPart {
public boolean cooldownWhenFull = true;
@Comment("The number of items moved per hopper transfer.")
public int amount = 1;
@Comment("Whether hoppers may load chunks to pull from or push into containers across a chunk border.")
public boolean canLoadChunks = false;
public boolean disableMoveEvent = false;
public boolean ignoreOccludingBlocks = false;
}
@@ -502,7 +682,6 @@ public class WorldConfiguration 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;
}
@@ -510,7 +689,14 @@ public class WorldConfiguration extends ConfigurationPart {
public Chunks chunks;
public class Chunks extends ConfigurationPart {
public AutosavePeriod autoSaveInterval = AutosavePeriod.def();
@Comment("Ticks between saves of this world. Set to 0 or below to disable automatic saving.")
public int autoSaveInterval = 6000;
@Comment("Per-world view distance. Defaults to the server.properties value.")
public IntOr.Default viewDistance = IntOr.Default.USE_DEFAULT;
@Comment("Per-world simulation distance. Defaults to the server.properties value.")
public IntOr.Default simulationDistance = IntOr.Default.USE_DEFAULT;
@Comment("Whether chunks kept loaded by a ticking-frozen world are unloaded.")
public boolean unloadFrozenChunks = false;
public int maxAutoSaveChunksPerTick = 24;
public int fixedChunkInhabitedTime = -1;
public int minChunkUnloadCount = 50;
@@ -545,6 +731,13 @@ public class WorldConfiguration extends ConfigurationPart {
public class TickRates extends ConfigurationPart {
public int grassSpread = 1;
@Comment("Ticks between hopper transfer attempts.")
public int hopperTransfer = 8;
@Comment("Ticks between hopper pickup checks.")
public int hopperCheck = 1;
@Comment("Ticks between hanging entity (item frame, painting) validity checks.")
public int hangingTickFrequency = 100;
public int containerUpdate = 1;
public int mobSpawner = 1;
public int wetFarmland = 1;
@@ -579,6 +772,15 @@ public class WorldConfiguration extends ConfigurationPart {
public Misc misc;
public class Misc extends ConfigurationPart {
@Comment("Radius in blocks that these sounds are broadcast to. Set to 0 to use the vanilla behaviour.")
public SoundRadius soundRadius;
public class SoundRadius extends ConfigurationPart {
public int dragonDeath = 0;
public int witherSpawn = 0;
public int endPortal = 0;
}
public boolean updatePathfindingOnBlockUpdate = true;
public boolean showSignClickCommandFailureMsgsToPlayer = false;
public RedstoneImplementation redstoneImplementation = RedstoneImplementation.VANILLA;
@@ -1,22 +1,33 @@
package io.papermc.paper.configuration.constraint;
import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import io.papermc.paper.configuration.type.number.LongOr;
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.math.BigDecimal;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.spongepowered.configurate.objectmapping.meta.Constraint;
import org.spongepowered.configurate.serialize.SerializationException;
import static io.leangen.geantyref.GenericTypeReflector.box;
import static io.leangen.geantyref.GenericTypeReflector.erase;
public final class Constraints {
private Constraints() {
}
public static final class Positive implements Constraint<Number> {
@Override
public void validate(@Nullable Number value) throws SerializationException {
public void validate(final @Nullable Number value) throws SerializationException {
if (value != null && value.doubleValue() <= 0) {
throw new SerializationException(value + " should be positive");
}
@@ -27,14 +38,19 @@ public final class Constraints {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Min {
int value();
double value();
final class Factory implements Constraint.Factory<Min, Number> {
final class Factory implements Constraint.Factory<Min, Object> {
@Override
public Constraint<Number> make(Min data, Type type) {
public Constraint<Object> make(final Min data, final Type type) {
final Function<Object, OptionalDouble> extractor = numberExtractor(type);
return value -> {
if (value != null && value.intValue() < data.value()) {
throw new SerializationException(value + " is less than the min " + data.value());
if (value == null) {
return;
}
final OptionalDouble number = extractor.apply(value);
if (number.isPresent() && number.getAsDouble() < data.value()) {
throw new SerializationException(format(number.getAsDouble()) + " is less than the min " + format(data.value()));
}
};
}
@@ -45,17 +61,47 @@ public final class Constraints {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Max {
int value();
double value();
final class Factory implements Constraint.Factory<Max, Number> {
final class Factory implements Constraint.Factory<Max, Object> {
@Override
public Constraint<Number> make(Max data, Type type) {
public Constraint<Object> make(final Max data, final Type type) {
final Function<Object, OptionalDouble> extractor = numberExtractor(type);
return value -> {
if (value != null && value.intValue() > data.value()) {
throw new SerializationException(value + " is greater than the max " + data.value());
if (value == null) {
return;
}
final OptionalDouble number = extractor.apply(value);
if (number.isPresent() && number.getAsDouble() > data.value()) {
throw new SerializationException(format(number.getAsDouble()) + " is greater than the max " + format(data.value()));
}
};
}
}
}
// empty for the "default"/"disabled" values of the NumberOr types, which are not range checked
private static Function<Object, OptionalDouble> numberExtractor(final Type type) {
final Class<?> erased = erase(box(type));
if (Number.class.isAssignableFrom(erased)) {
return value -> OptionalDouble.of(((Number) value).doubleValue());
} else if (IntOr.class.isAssignableFrom(erased)) {
return value -> {
final OptionalInt number = ((IntOr) value).value();
return number.isPresent() ? OptionalDouble.of(number.getAsInt()) : OptionalDouble.empty();
};
} else if (LongOr.class.isAssignableFrom(erased)) {
return value -> {
final OptionalLong number = ((LongOr) value).value();
return number.isPresent() ? OptionalDouble.of(number.getAsLong()) : OptionalDouble.empty();
};
} else if (DoubleOr.class.isAssignableFrom(erased)) {
return value -> ((DoubleOr) value).value();
}
throw new IllegalArgumentException(type.getTypeName() + " is not a number type that supports range constraints");
}
private static String format(final double value) {
return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString();
}
}
@@ -0,0 +1,4 @@
@NullMarked
package io.papermc.paper.configuration.constraint;
import org.jspecify.annotations.NullMarked;
@@ -1,29 +0,0 @@
package io.papermc.paper.configuration.legacy;
import org.jspecify.annotations.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;
}
}
@@ -1,48 +0,0 @@
package io.papermc.paper.configuration.legacy;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
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 org.jspecify.annotations.Nullable;
import org.spigotmc.SpigotWorldConfig;
import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RequiresSpigotInitialization {
Class<? extends NodeResolver> value();
final class Factory implements NodeResolver.Factory {
private final SpigotWorldConfig spigotWorldConfig;
private final Table<Class<? extends NodeResolver>, String, NodeResolver> cache = HashBasedTable.create();
public Factory(SpigotWorldConfig spigotWorldConfig) {
this.spigotWorldConfig = spigotWorldConfig;
}
@Override
public @Nullable NodeResolver make(String name, AnnotatedElement element) {
if (element.isAnnotationPresent(RequiresSpigotInitialization.class)) {
return this.cache.row(element.getAnnotation(RequiresSpigotInitialization.class).value()).computeIfAbsent(name, key -> {
try {
final Constructor<? extends NodeResolver> constructor = element.getAnnotation(RequiresSpigotInitialization.class).value().getDeclaredConstructor(String.class, SpigotWorldConfig.class);
constructor.trySetAccessible();
return constructor.newInstance(key, this.spigotWorldConfig);
} catch (final ReflectiveOperationException e) {
throw new RuntimeException("Could not create constraint", e);
}
});
}
return null;
}
}
}
@@ -1,27 +0,0 @@
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;
}
}
@@ -2,8 +2,10 @@ package io.papermc.paper.configuration.mapping;
import io.leangen.geantyref.GenericTypeReflector;
import io.papermc.paper.configuration.ConfigurationPart;
import io.papermc.paper.configuration.GlobalConfiguration;
import io.papermc.paper.configuration.WorldConfiguration;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -12,6 +14,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SequencedMap;
import java.util.function.BiPredicate;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.spongepowered.configurate.objectmapping.FieldDiscoverer;
@@ -25,11 +28,13 @@ public final class InnerClassFieldDiscoverer implements FieldDiscoverer<Map<Fiel
private final InnerClassInstanceSupplier instanceSupplier;
private final FieldDiscoverer<Map<Field, Object>> delegate;
private final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>>> fieldProcessors;
private final BiPredicate<AnnotatedType, AnnotatedElement> fieldFilter;
private InnerClassFieldDiscoverer(final InnerClassInstanceSupplier instanceSupplier, final FieldDiscoverer<Map<Field, Object>> delegate, final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>>> fieldProcessors) {
private InnerClassFieldDiscoverer(final InnerClassInstanceSupplier instanceSupplier, final FieldDiscoverer<Map<Field, Object>> delegate, final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>>> fieldProcessors, final BiPredicate<AnnotatedType, AnnotatedElement> fieldFilter) {
this.instanceSupplier = instanceSupplier;
this.delegate = delegate;
this.fieldProcessors = fieldProcessors;
this.fieldFilter = fieldFilter;
}
@SuppressWarnings({"unchecked", "rawtypes"})
@@ -39,7 +44,7 @@ public final class InnerClassFieldDiscoverer implements FieldDiscoverer<Map<Fiel
if (ConfigurationPart.class.isAssignableFrom(clazz)) {
// we don't care about the returned instance factory, we just need to call it to collect fields
final Object dummyInstance = this.delegate.<V>discover(target, (name, fieldType, container, deserializer, serializer) -> {
if (!erase(fieldType.getType()).equals(clazz.getEnclosingClass())) { // don't collect synth fields for inner classes
if (!erase(fieldType.getType()).equals(clazz.getEnclosingClass()) && this.fieldFilter.test(fieldType, container)) { // don't collect synth fields for inner classes
FieldProcessor<?> processor = null;
processor: for (final Annotation annotation : container.getAnnotations()) {
final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> definitions = this.fieldProcessors.get(annotation.annotationType());
@@ -80,14 +85,18 @@ public final class InnerClassFieldDiscoverer implements FieldDiscoverer<Map<Fiel
record ImplicitProvider(Supplier<Object> provider) {
}
@SuppressWarnings("unchecked")
private static InnerClassFieldDiscoverer create(final Map<Class<?>, Object> overrides, final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> fieldProcessors) {
return create(overrides, fieldProcessors, (fieldType, container) -> true);
}
@SuppressWarnings("unchecked")
private static InnerClassFieldDiscoverer create(final Map<Class<?>, Object> overrides, final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> fieldProcessors, final BiPredicate<AnnotatedType, AnnotatedElement> fieldFilter) {
final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>>> processors = new LinkedHashMap<>();
for (final Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>> definition : fieldProcessors) {
processors.computeIfAbsent(definition.annotation(), k -> new ArrayList<>()).add(definition);
}
final InnerClassInstanceSupplier instanceSupplier = new InnerClassInstanceSupplier(overrides);
return new InnerClassFieldDiscoverer(instanceSupplier, (FieldDiscoverer<Map<Field, Object>>) FieldDiscoverer.object(instanceSupplier), processors);
return new InnerClassFieldDiscoverer(instanceSupplier, (FieldDiscoverer<Map<Field, Object>>) FieldDiscoverer.object(instanceSupplier), processors, fieldFilter);
}
public static FieldDiscoverer<?> worldConfig(final WorldConfiguration worldConfiguration, final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> fieldProcessors) {
@@ -100,4 +109,21 @@ public final class InnerClassFieldDiscoverer implements FieldDiscoverer<Map<Fiel
public static FieldDiscoverer<?> globalConfig(final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> fieldProcessors) {
return create(Collections.emptyMap(), fieldProcessors);
}
public static FieldDiscoverer<?> globalConfig(final GlobalConfiguration globalConfiguration, final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> fieldProcessors) {
final Map<Class<?>, Object> overrides = Map.of(
GlobalConfiguration.class, globalConfiguration
);
return create(overrides, fieldProcessors);
}
/**
* Only discovers {@link LoadEarly} fields, and the sections containing them.
*/
public static FieldDiscoverer<?> earlyGlobalConfig(final GlobalConfiguration globalConfiguration, final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> fieldProcessors) {
final Map<Class<?>, Object> overrides = Map.of(
GlobalConfiguration.class, globalConfiguration
);
return create(overrides, fieldProcessors, LoadEarly.FIELD_FILTER);
}
}
@@ -32,6 +32,7 @@ final class InnerClassInstanceFactory implements FieldDiscoverer.MutableInstance
@Override
public void complete(final Object instance, final Map<Field, DeserializedFieldInfo<?>> intermediate) throws SerializationException {
// modeled off of native ObjectFieldDiscoverer
SerializationException failure = null;
for (final Map.Entry<Field, DeserializedFieldInfo<?>> entry : intermediate.entrySet()) {
final boolean hasProcessor = entry.getValue().processor() != null;
try {
@@ -57,9 +58,17 @@ final class InnerClassInstanceFactory implements FieldDiscoverer.MutableInstance
throw new SerializationException(this.targetType.getType(), e);
} catch (final SerializationException ex) {
ex.initType(entry.getValue().fieldType());
// like ObjectMapperImpl, keep going so every failing field is reported
if (failure == null) {
failure = ex;
} else {
failure.addSuppressed(ex);
}
}
}
if (failure != null) {
throw failure;
}
}
@Override
@@ -0,0 +1,72 @@
package io.papermc.paper.configuration.mapping;
import io.papermc.paper.configuration.ConfigurationPart;
import io.papermc.paper.configuration.GlobalConfiguration;
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.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.function.BiPredicate;
import static io.leangen.geantyref.GenericTypeReflector.erase;
/**
* Marks a field of the {@link GlobalConfiguration} that is deserialized early during startup,
* before {@link net.minecraft.server.Bootstrap} has run and before a {@link net.minecraft.core.RegistryAccess} exists.
* The same instance is populated again with every other field once the full config is loaded.
* <p>
* Every {@link ConfigurationPart} containing an early field is instantiated during the early load,
* which runs <b>all</b> of its field initializers, not just the ones of early fields. Those must therefore
* not touch anything that requires the game to be bootstrapped, like registries.
* <p>
* Early fields may only use registry-free types, and {@link org.spongepowered.configurate.objectmapping.meta.PostProcess}
* methods are not run during the early load. Marking a {@link ConfigurationPart} field itself has no effect, mark its fields instead.
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoadEarly {
/**
* Accepts only fields marked with {@link LoadEarly}, and {@link ConfigurationPart}s that (transitively) contain such a field.
* Rejected sections are never instantiated.
*/
BiPredicate<AnnotatedType, AnnotatedElement> FIELD_FILTER = new FieldFilter();
final class FieldFilter implements BiPredicate<AnnotatedType, AnnotatedElement> {
private static final ClassValue<Boolean> CONTAINS_EARLY_FIELDS = new ClassValue<>() {
@Override
protected Boolean computeValue(final Class<?> type) {
for (Class<?> clazz = type; clazz != null && clazz != Object.class; clazz = clazz.getSuperclass()) {
for (final Field field : clazz.getDeclaredFields()) {
if (field.isSynthetic() || Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
continue;
}
if (field.isAnnotationPresent(LoadEarly.class) || isEarlySection(field.getType())) {
return true;
}
}
}
return false;
}
};
private FieldFilter() {
}
private static boolean isEarlySection(final Class<?> type) {
return ConfigurationPart.class.isAssignableFrom(type) && CONTAINS_EARLY_FIELDS.get(type);
}
@Override
public boolean test(final AnnotatedType fieldType, final AnnotatedElement container) {
return container.isAnnotationPresent(LoadEarly.class) || isEarlySection(erase(fieldType.getType()));
}
}
}
@@ -172,5 +172,4 @@ public class MapSerializer implements TypeSerializer.Annotated<Map<?, ?>> {
}
return new LinkedHashMap<>();
}
}
@@ -0,0 +1,32 @@
package io.papermc.paper.configuration.transformation;
import io.papermc.paper.configuration.Configurations.ContextKey;
import io.papermc.paper.configuration.Configurations.ContextMap;
import java.util.Set;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
/**
* A versioned transformation that declares the context it needs instead of reaching for global state,
* so it can be left out of loads where that context is not available yet.
*/
public interface ContextualTransformation {
int version();
/**
* Keys that must be present in the {@link ContextMap} passed to {@link #create(ContextMap)}.
*
* @return required context keys
*/
default Set<ContextKey<?>> requiredContext() {
return Set.of();
}
/**
* Creates the transformation.
*
* @param context context containing at least every key in {@link #requiredContext()}
* @return the transformation
*/
ConfigurationTransformation create(ContextMap context);
}
@@ -0,0 +1,41 @@
package io.papermc.paper.configuration.transformation;
import java.util.Properties;
import java.util.function.Consumer;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.dedicated.DedicatedServerProperties;
/**
* Access to {@code server.properties} for transformations moving options between it and the Paper configs.
*/
public interface ServerPropertiesAccess {
DedicatedServerProperties current();
/**
* Edits a copy of the raw properties, then reloads and saves {@code server.properties} from it.
*
* @param editor edits the raw properties
*/
void update(Consumer<Properties> editor);
static ServerPropertiesAccess of(final DedicatedServer server) {
return new ServerPropertiesAccess() {
@Override
public DedicatedServerProperties current() {
return server.settings.getProperties();
}
@Override
public void update(final Consumer<Properties> editor) {
server.settings.update(config -> {
// same as Settings#cloneProperties, used by Settings.MutableValue#update
final Properties properties = new Properties();
properties.putAll(config.properties);
editor.accept(properties);
return config.reload(server.registryAccess(), properties, server.options);
});
}
};
}
}
@@ -1,15 +1,26 @@
package io.papermc.paper.configuration.transformation;
import com.mojang.logging.LogUtils;
import io.papermc.paper.configuration.Configuration;
import io.papermc.paper.configuration.Configurations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import static com.google.common.base.Preconditions.checkState;
import static org.spongepowered.configurate.NodePath.path;
public final class Transformations {
private static final Logger LOGGER = LogUtils.getClassLogger();
private Transformations() {
}
@@ -34,8 +45,114 @@ public final class Transformations {
return ConfigurationTransformation.versionedBuilder().versionKey(Configuration.VERSION_FIELD);
}
@FunctionalInterface
public interface DefaultsAware {
void apply(final ConfigurationTransformation.Builder builder, final Configurations.ContextMap contextMap, final ConfigurationNode defaultsNode);
public static void pruneEmptySections(final ConfigurationNode node, final String... sections) {
for (final String section : sections) {
final ConfigurationNode child = node.node(section);
if (!child.virtual() && isEffectivelyEmpty(child)) {
node.removeChild(section);
}
}
}
private static boolean isEffectivelyEmpty(final ConfigurationNode node) {
if (node.isMap()) {
return node.childrenMap().values().stream().allMatch(Transformations::isEffectivelyEmpty);
}
return node.empty();
}
/**
* Removes every option a legacy migration left at its original path, so options nothing reads
* anymore are not carried into the new config as unknown keys.
*
* @param legacy the legacy node before migrating
* @param migrated the same node after migrating
* @param source the legacy file the options came from, for logging
* @param keptInPlace options that legitimately keep their path
*/
public static void removeUnmigrated(final ConfigurationNode legacy, final ConfigurationNode migrated, final String source, final Set<NodePath> keptInPlace) {
final List<NodePath> leaves = new ArrayList<>();
collectLeaves(legacy, new ArrayList<>(), leaves);
final List<String> removed = new ArrayList<>();
for (final NodePath leaf : leaves) {
if (keptInPlace.contains(leaf) || migrated.node(leaf.array()).virtual()) {
continue;
}
final Object[] path = leaf.array();
migrated.node(path).raw(null);
removed.add(Arrays.stream(path).map(String::valueOf).collect(Collectors.joining(".")));
// drop parents emptied by the removal
for (int length = path.length - 1; length > 0; length--) {
final ConfigurationNode parent = migrated.node(Arrays.copyOf(path, length));
if (!isEffectivelyEmpty(parent)) {
break;
}
parent.raw(null);
}
}
if (!removed.isEmpty()) {
LOGGER.warn("Dropped options from {} that are no longer used: {}", source, String.join(", ", removed));
}
}
private static void collectLeaves(final ConfigurationNode node, final List<Object> path, final List<NodePath> leaves) {
if (node.isMap()) {
for (final Map.Entry<Object, ? extends ConfigurationNode> child : node.childrenMap().entrySet()) {
path.add(child.getKey());
collectLeaves(child.getValue(), path, leaves);
path.removeLast();
}
} else if (!path.isEmpty() && !node.empty()) {
leaves.add(NodePath.of(path));
}
}
public static ConfigurationTransformation single(final NodePath path, final TransformAction action) {
return ConfigurationTransformation.builder().addAction(path, action).build();
}
public static TransformAction setRaw(final Object value) {
return (path, node) -> {
node.raw(value);
return null;
};
}
public static NodePath prefix(final String prefix, final String... other) {
final List<String> list = new ArrayList<>(other.length + 1);
list.add(prefix);
list.addAll(Arrays.asList(other));
return NodePath.of(list);
}
public static TransformAction move(final NodePath to) {
return (path, node) -> to.array();
}
// replaces the last path element with all the arguments
public static TransformAction renameDeep(final String first, final String... other) {
return (path, node) -> {
final Object[] current = path.array();
final Object[] newPath = new Object[current.length + other.length];
System.arraycopy(current, 0, newPath, 0, current.length - 1);
newPath[current.length - 1] = first;
System.arraycopy(other, 0, newPath, current.length, other.length);
return newPath;
};
}
public static TransformAction move(final TransformAction action, final NodePath newPath) {
return (path, node) -> {
action.visitPath(path, node);
return newPath.array();
};
}
public static TransformAction moveParent(final String... parents) {
return (path, value) -> {
checkState(path.size() > 0, "can't move root");
final NodePath newPath = path((Object[]) parents);
return newPath.withAppendedChild(path.get(path.size() - 1)).array();
};
}
}
@@ -0,0 +1,111 @@
package io.papermc.paper.configuration.transformation.global;
import io.papermc.paper.configuration.transformation.Transformations;
import java.util.List;
import java.util.Locale;
import java.util.Set;
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 static io.papermc.paper.configuration.transformation.Transformations.move;
import static org.spongepowered.configurate.NodePath.path;
import static org.spongepowered.configurate.transformation.TransformAction.remove;
public final class LegacyBukkitConfig {
private LegacyBukkitConfig() {
}
public static void migrate(final ConfigurationNode node) throws ConfigurateException {
final ConfigurationNode legacy = node.copy();
toNewFormat().apply(node);
// world settings, handled by migrateWorldDefaults
node.removeChild("spawn-limits");
node.removeChild("ticks-per");
Transformations.removeUnmigrated(legacy, node, "bukkit.yml", Set.of());
Transformations.pruneEmptySections(node, "settings", "chunk-gc", "worlds");
}
public static ConfigurationTransformation toNewFormat() {
return ConfigurationTransformation.builder()
.addAction(settings("world-container"), move(path("misc", "world-container")))
.addAction(settings("update-folder"), move(path("misc", "update-folder")))
.addAction(settings("allow-end"), move(path("misc", "enable-end")))
.addAction(settings("permissions-file"), move(path("misc", "permissions-file")))
.addAction(settings("connection-throttle"), move(path("misc", "connection-throttle")))
.addAction(settings("query-plugins"), move(path("misc", "query-plugins")))
.addAction(settings("use-map-color-cache"), move(path("misc", "use-map-color-cache")))
.addAction(settings("minimum-api"), move(path("misc", "minimum-api")))
.addAction(settings("deprecated-verbose"), (nodePath, value) -> {
// was "true"/"false"/"default", it is a WarningState now
value.set(switch (value.getString("default").toLowerCase(Locale.ROOT)) {
case "true" -> "ON";
case "false" -> "OFF";
default -> "DEFAULT";
});
return path("misc", "deprecated-verbose").array();
})
.addAction(settings("shutdown-message"), move(LegacySpigotConfig.miniMessage(), path("messages", "shutdown")))
.addAction(path("chunk-gc", "period-in-ticks"), move(path("chunk-system", "plugin-ticket-timeout")))
.addAction(path("ticks-per", "autosave"), move(path("misc", "auto-save-interval")))
.addAction(path("worlds", ConfigurationTransformation.WILDCARD_OBJECT, "generator"), intoGenerators())
.addAction(path("worlds", ConfigurationTransformation.WILDCARD_OBJECT, "biome-provider"), intoGenerators())
// dropped, nothing reads these
.addAction(settings("warn-on-overload"), remove())
.addAction(settings("plugin-profiling"), remove())
.addAction(settings("compatibility"), remove()) // CraftServer#loadCompatibilities has been a no-op for years
.build();
}
private static NodePath settings(final String key) {
return path("settings", key);
}
public static ConfigurationTransformation worldDefaultsToNewFormat() {
// Keys that belong to the world config; applied to a separate copy and merged into the world defaults
return ConfigurationTransformation.builder()
.addAction(path("spawn-limits", "monsters"), spawning("spawn-limits", "monster"))
.addAction(path("spawn-limits", "animals"), spawning("spawn-limits", "creature"))
.addAction(path("spawn-limits", "water-animals"), spawning("spawn-limits", "water_creature"))
.addAction(path("spawn-limits", "water-ambient"), spawning("spawn-limits", "water_ambient"))
.addAction(path("spawn-limits", "water-underground-creature"), spawning("spawn-limits", "underground_water_creature"))
.addAction(path("spawn-limits", "axolotls"), spawning("spawn-limits", "axolotls"))
.addAction(path("spawn-limits", "ambient"), spawning("spawn-limits", "ambient"))
.addAction(path("ticks-per", "monster-spawns"), spawning("ticks-per-spawn", "monster"))
.addAction(path("ticks-per", "animal-spawns"), spawning("ticks-per-spawn", "creature"))
.addAction(path("ticks-per", "water-spawns"), spawning("ticks-per-spawn", "water_creature"))
.addAction(path("ticks-per", "water-ambient-spawns"), spawning("ticks-per-spawn", "water_ambient"))
.addAction(path("ticks-per", "water-underground-creature-spawns"), spawning("ticks-per-spawn", "underground_water_creature"))
.addAction(path("ticks-per", "axolotl-spawns"), spawning("ticks-per-spawn", "axolotls"))
.addAction(path("ticks-per", "ambient-spawns"), spawning("ticks-per-spawn", "ambient"))
.addAction(path("ticks-per", "autosave"), move(path("chunks", "auto-save-interval")))
.build();
}
// Expects a copy of the whole bukkit.yml, keeps only what belongs in the world defaults
public static void migrateWorldDefaults(final ConfigurationNode node) throws ConfigurateException {
worldDefaultsToNewFormat().apply(node);
for (final Object key : List.copyOf(node.childrenMap().keySet())) {
if (!"entities".equals(key) && !"chunks".equals(key)) {
node.removeChild(key);
}
}
}
private static TransformAction spawning(final String section, final String category) {
return (nodePath, value) -> {
if (value.getInt(0) < 0) { // meant the vanilla value, which is the default now
value.raw(null);
return null;
}
return path("entities", "spawning", section, category).array();
};
}
private static TransformAction intoGenerators() {
return (nodePath, value) -> new Object[]{"world-generators", nodePath.get(1), nodePath.get(2)};
}
}
@@ -171,8 +171,13 @@ public final class LegacyPaperConfig {
private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final String i18nKey, final String... strPath) {
miniMessageWithTranslatable(builder, englishCheck, Component.translatable(i18nKey), strPath);
}
private static void miniMessageWithTranslatable(final ConfigurationTransformation.Builder builder, final Predicate<String> englishCheck, final Component component, final String... strPath) {
builder.addAction(path((Object[]) strPath), (path, value) -> {
builder.addAction(path((Object[]) strPath), miniMessageWithTranslatable(englishCheck, component));
}
static TransformAction miniMessageWithTranslatable(final Predicate<? super String> englishCheck, final Component component) {
return (path, value) -> {
final Object val = value.raw();
if (val != null) {
final String strVal = val.toString();
@@ -183,7 +188,7 @@ public final class LegacyPaperConfig {
}
value.set(MiniMessage.miniMessage().serialize(component));
return null;
});
};
}
private static void miniMessage(final ConfigurationTransformation.Builder builder, final String... strPath) {
@@ -197,7 +202,7 @@ public final class LegacyPaperConfig {
}
@SuppressWarnings("deprecation") // valid use to convert legacy string to mini-message in legacy migration
private static String miniMessage(final String input) {
static String miniMessage(final String input) {
return MiniMessage.miniMessage().serialize(LegacyComponentSerializer.legacySection().deserialize(ChatColor.translateAlternateColorCodes('&', input)));
}
@@ -0,0 +1,211 @@
package io.papermc.paper.configuration.transformation.global;
import io.papermc.paper.configuration.Configuration;
import io.papermc.paper.configuration.transformation.Transformations;
import org.spongepowered.configurate.ConfigurateException;
import java.util.List;
import java.util.Set;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import static io.papermc.paper.configuration.transformation.Transformations.move;
import static io.papermc.paper.configuration.transformation.Transformations.moveParent;
import static io.papermc.paper.configuration.transformation.Transformations.prefix;
import static io.papermc.paper.configuration.transformation.Transformations.setRaw;
import static io.papermc.paper.configuration.transformation.Transformations.single;
import static io.papermc.paper.configuration.transformation.global.LegacyPaperConfig.miniMessageWithTranslatable;
import static net.kyori.adventure.text.Component.translatable;
import static org.spongepowered.configurate.NodePath.path;
import static org.spongepowered.configurate.transformation.TransformAction.remove;
import static org.spongepowered.configurate.transformation.TransformAction.rename;
import static org.spongepowered.configurate.transformation.TransformAction.set;
// Transforms spigot.yml non-world settings into the new config format
public final class LegacySpigotConfig {
private static final Set<NodePath> KEPT_IN_PLACE = Set.of(
path("players", "disable-saving"),
path("advancements", "disable-saving"),
path("advancements", "disabled"),
path("stats", "disable-saving")
);
private LegacySpigotConfig() {}
public static void migrate(final ConfigurationNode node) throws ConfigurateException {
final ConfigurationNode legacy = node.copy();
transformation().apply(node);
toNewFormat().apply(node);
node.removeChild(Configuration.LEGACY_CONFIG_VERSION_FIELD);
Transformations.removeUnmigrated(legacy, node, "spigot.yml", KEPT_IN_PLACE);
Transformations.pruneEmptySections(node, "settings", "commands", "messages", "stats", "advancements", "players");
}
public static ConfigurationTransformation transformation() {
return ConfigurationTransformation.chain(versioned(), notVersioned());
}
// Version transforms taken directly from the old SpigotConfig class
private static ConfigurationTransformation.Versioned versioned() {
return ConfigurationTransformation.versionedBuilder()
.versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD)
.addVersion(4, single(settings("bungeecord"), setRaw(false)))
.addVersion(6, single(commands("tab-complete"), (path, value) -> {
if (value.getBoolean(true)) {
value.raw(0);
} else {
value.raw(-1);
}
return null;
}))
.addVersion(
8,
single(path(messages("outdated-client")), setRaw("Outdated client! Please use {0}")),
single(path(messages("outdated-server")), setRaw("Outdated server! I'm still on {0}"))
)
.build();
}
// Other non-versioned transforms
private static ConfigurationTransformation notVersioned() {
return ConfigurationTransformation.builder()
.addAction(path("replace-commands"), remove()) // not supported anymore
.addAction(commands("replace-commands"), remove()) // not supported anymore
.addAction(commands(), (path, value) -> {
// adding enabled toggle
final List<String> spamExclusions = value.node("spam-exclusions").getList(String.class, List.of("/skill"));
final ConfigurationNode enabledNode = value.node("enable-spam-exclusions");
if (!enabledNode.virtual()) {
return null;
}
if (spamExclusions.isEmpty()) {
enabledNode.set(false);
return null;
}
enabledNode.set(spamExclusions.size() > 1 || !"/skill".equals(spamExclusions.getFirst()));
return null;
})
.build();
}
public static ConfigurationTransformation toNewFormat() {
return ConfigurationTransformation.versionedBuilder()
.versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD)
.addVersion(Configuration.FINAL_LEGACY_SPIGOT_VERSION + 1, newFormatTransformation())
.build();
}
private static ConfigurationTransformation newFormatTransformation() {
return ConfigurationTransformation.builder()
// commands
.addAction(commands("log"), move(path("logging", "command-execution")))
.addAction(commands("send-namespaced"), rename("send-namespaced-commands"))
.addAction(commands("tab-complete"), (path, value) -> {
value.raw(value.getInt(0) >= 0); // the int value isn't used anymore, it's only a bool now
return commands("tab-completion").array();
})
.addAction(commands("silent-commandblock-console"), rename("silent-command-block-console"))
.addAction(commands("spam-exclusions"), move(path("spam-limiter", "commands", "exclusions")))
.addAction(commands("enable-spam-exclusions"), move(path("spam-limiter", "commands", "enabled")))
// messages
.addAction(
messages("whitelist"),
move(
miniMessageWithTranslatable(
Set.of("You are not whitelisted on this server!", "You are not white-listed on this server!")::contains,
translatable("multiplayer.disconnect.not_whitelisted")
),
messages("kick", "whitelist")
)
)
.addAction(messages("unknown-command"), (path, value) -> {
// only used to control if there should be a message at all; convert to bool
value.raw(!value.getString("").isEmpty());
return messages("send-command-parse-failure-message").array();
})
.addAction(messages("server-full"), move(
miniMessageWithTranslatable("The server is full!"::equals, translatable("multiplayer.disconnect.server_full")),
messages("kick", "server-full")
))
.addAction(
messages("outdated-client"),
move(
versionKickMessage("Outdated client! Please use %s", "multiplayer.disconnect.outdated_client"),
messages("kick", "outdated-client")
)
)
.addAction(
messages("outdated-server"),
move(
versionKickMessage("Outdated server! I'm still on %s", "multiplayer.disconnect.outdated_server"),
messages("kick", "outdated-server")
)
)
.addAction(messages("restart"), move(miniMessage(), messages("kick", "restart")))
// players.disable-saving
// advancements.disable-saving
// advancements.disabled
// stats.disable-saving
.addAction(path("stats", "forced-stats"), rename("forced-custom-stat-values"))
// settings
.addAction(settings("debug"), move(path("logging", "debug")))
.addAction(settings("bungeecord"), move(path("proxies", "bungee-cord", "enabled")))
.addAction(settings("log-villager-deaths"), move(path("logging", "villager-deaths")))
.addAction(settings("log-named-deaths"), move(path("logging", "named-living-entity-deaths")))
.addAction(settings("moved-wrongly-threshold"), moveParent("anticheat"))
.addAction(settings("moved-too-quickly-multiplier"), moveParent("anticheat"))
.addAction(settings("timeout-time"), move(path("watchdog", "timeout-seconds")))
.addAction(settings("restart-on-crash"), moveParent("watchdog"))
.addAction(settings("restart-script"), moveParent("watchdog"))
.addAction(settings("user-cache-size"), moveParent("players"))
.addAction(settings("save-user-cache-on-stop-only"), moveParent("players"))
.addAction(settings("netty-threads"), moveParent("misc"))
.addAction(settings("sample-count"), moveParent("players"))
.addAction(settings("player-shuffle"), move(path("players", "connection-shuffle")))
.addAction(settings("attribute", "maxAbsorption", "max"), move(path("attributes", "overrides", "minecraft:max_absorption", "max")))
.addAction(settings("attribute", "maxHealth", "max"), move(path("attributes", "overrides", "minecraft:max_health", "max")))
.addAction(settings("attribute", "movementSpeed", "max"), move(path("attributes", "overrides", "minecraft:movement_speed", "max")))
.addAction(settings("attribute", "attackDamage", "max"), move(path("attributes", "overrides", "minecraft:attack_damage", "max")))
.build();
}
private static NodePath settings(final String... other) {
return prefix("settings", other);
}
private static NodePath commands(final String... other) {
return prefix("commands", other);
}
private static NodePath messages(final String... other) {
return prefix("messages", other);
}
// spigot used {0} for the server version, the kick message substitutes it into %s
private static TransformAction versionKickMessage(final String englishDefault, final String translationKey) {
final TransformAction convert = miniMessageWithTranslatable(
Set.of(englishDefault, "Incompatible client! Please use %s")::contains,
translatable(translationKey)
);
return (path, value) -> {
final String message = value.getString();
if (message != null) {
value.raw(message.replaceAll("\\{\\d+}", "%s"));
}
return convert.visitPath(path, value);
};
}
static TransformAction miniMessage() {
return (path, value) -> {
final Object val = value.raw();
if (val != null) {
value.set(LegacyPaperConfig.miniMessage(val.toString()));
}
return null;
};
}
}
@@ -1,44 +1,45 @@
package io.papermc.paper.configuration.transformation.global.versioned;
import java.util.Properties;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
import org.jspecify.annotations.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import io.papermc.paper.configuration.Configurations;
import io.papermc.paper.configuration.Configurations.ContextKey;
import io.papermc.paper.configuration.Configurations.ContextMap;
import io.papermc.paper.configuration.transformation.ContextualTransformation;
import io.papermc.paper.configuration.transformation.ServerPropertiesAccess;
import java.util.Set;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import static org.spongepowered.configurate.NodePath.path;
public class V29_LogIPs implements TransformAction {
public final class V29_LogIPs implements ContextualTransformation {
public static final V29_LogIPs INSTANCE = new V29_LogIPs();
private static final int VERSION = 29;
private static final NodePath PATH = path("logging", "log-player-ip-addresses");
private static final V29_LogIPs INSTANCE = new V29_LogIPs();
private V29_LogIPs() {
}
public static void apply(final ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build());
@Override
public int version() {
return VERSION;
}
@Override
public Object @Nullable [] visitPath(final NodePath path, final ConfigurationNode value) throws ConfigurateException {
final DedicatedServer server = ((DedicatedServer) MinecraftServer.getServer());
final boolean val = value.getBoolean(server.settings.getProperties().logIPs);
server.settings.update((config) -> {
final Properties newProps = new Properties(config.properties);
newProps.setProperty("log-ips", String.valueOf(val));
return config.reload(server.registryAccess(), newProps, server.options);
});
value.raw(null);
return null;
public Set<ContextKey<?>> requiredContext() {
return Set.of(Configurations.SERVER_PROPERTIES);
}
@Override
public ConfigurationTransformation create(final ContextMap context) {
final ServerPropertiesAccess serverProperties = context.require(Configurations.SERVER_PROPERTIES);
return ConfigurationTransformation.builder().addAction(PATH, (path, value) -> {
final boolean val = value.getBoolean(serverProperties.current().logIPs);
serverProperties.update(properties -> properties.setProperty("log-ips", String.valueOf(val)));
value.raw(null);
return null;
}).build();
}
}
@@ -5,24 +5,23 @@ import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.papermc.paper.configuration.Configurations.ContextMap;
import io.papermc.paper.configuration.transformation.ContextualTransformation;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Map;
import java.util.Objects;
import net.minecraft.resources.Identifier;
import org.jspecify.annotations.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import static java.util.Objects.requireNonNull;
import static org.spongepowered.configurate.NodePath.path;
public class V30_PacketIds implements TransformAction {
public final class V30_PacketIds implements ContextualTransformation {
public static final V30_PacketIds INSTANCE = new V30_PacketIds();
private static final int VERSION = 30;
@@ -48,16 +47,20 @@ public class V30_PacketIds implements TransformAction {
MOJANG_TO_ID = builder2.build();
}
public static void apply(final ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(path("packet-limiter", "overrides", ConfigurationTransformation.WILDCARD_OBJECT), INSTANCE).build());
}
private V30_PacketIds() {
}
@Override
public int version() {
return VERSION;
}
@Override
public Object @Nullable [] visitPath(final NodePath path, final ConfigurationNode value) {
public ConfigurationTransformation create(final ContextMap context) {
return ConfigurationTransformation.builder().addAction(path("packet-limiter", "overrides", ConfigurationTransformation.WILDCARD_OBJECT), V30_PacketIds::renamePacket).build();
}
private static Object @Nullable [] renamePacket(final NodePath path, final ConfigurationNode value) {
final String oldClassName = path.get(path.size() - 1).toString();
if (MOJANG_TO_ID.containsKey(oldClassName)) {
return path.with(path.size() - 1, MOJANG_TO_ID.get(oldClassName).toString()).array();
@@ -1,33 +1,42 @@
package io.papermc.paper.configuration.transformation.global.versioned;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
import org.spongepowered.configurate.ConfigurateException;
import io.papermc.paper.configuration.Configurations;
import io.papermc.paper.configuration.Configurations.ContextKey;
import io.papermc.paper.configuration.Configurations.ContextMap;
import io.papermc.paper.configuration.transformation.ContextualTransformation;
import io.papermc.paper.configuration.transformation.ServerPropertiesAccess;
import java.util.Set;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import java.util.Properties;
public final class V31_AllowNetherPropertiesToConfig implements ContextualTransformation {
public class V31_AllowNetherPropertiesToConfig implements ConfigurationTransformation {
public static final V31_AllowNetherPropertiesToConfig INSTANCE = new V31_AllowNetherPropertiesToConfig();
private static final int VERSION = 31;
public static void apply(final ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, INSTANCE);
private V31_AllowNetherPropertiesToConfig() {
}
@Override
public void apply(final ConfigurationNode root) throws ConfigurateException {
final DedicatedServer server = ((DedicatedServer) MinecraftServer.getServer());
final String raw = server.settings.getProperties().properties.getProperty("allow-nether");
if (raw != null) {
final ConfigurationNode node = root.node("misc", "enable-nether");
node.set(raw.equals("true"));
server.settings.update((config) -> {
final Properties newProps = new Properties(config.properties);
newProps.remove("allow-nether");
return config.reload(server.registryAccess(), newProps, server.options);
});
}
public int version() {
return VERSION;
}
@Override
public Set<ContextKey<?>> requiredContext() {
return Set.of(Configurations.SERVER_PROPERTIES);
}
@Override
public ConfigurationTransformation create(final ContextMap context) {
final ServerPropertiesAccess serverProperties = context.require(Configurations.SERVER_PROPERTIES);
return root -> {
final String raw = serverProperties.current().properties.getProperty("allow-nether");
if (raw != null) {
final ConfigurationNode node = root.node("misc", "enable-nether");
node.set(raw.equals("true"));
serverProperties.update(properties -> properties.remove("allow-nether"));
}
};
}
}
@@ -0,0 +1,32 @@
package io.papermc.paper.configuration.transformation.global.versioned;
import io.papermc.paper.configuration.Configurations.ContextMap;
import io.papermc.paper.configuration.transformation.ContextualTransformation;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.MoveStrategy;
import static io.papermc.paper.configuration.transformation.Transformations.move;
import static org.spongepowered.configurate.NodePath.path;
public final class V32_SpigotConfigCleanup implements ContextualTransformation {
public static final V32_SpigotConfigCleanup INSTANCE = new V32_SpigotConfigCleanup();
private static final int VERSION = 32;
private V32_SpigotConfigCleanup() {
}
@Override
public int version() {
return VERSION;
}
@Override
public ConfigurationTransformation create(final ContextMap context) {
return ConfigurationTransformation.builder()
.moveStrategy(MoveStrategy.MERGE)
.addAction(path("player-auto-save"), move(path("players", "auto-save")))
.addAction(path("misc", "strict-advancement-dimension-check"), move(path("advancements", "strict-dimension-check")))
.build();
}
}
@@ -0,0 +1,156 @@
package io.papermc.paper.configuration.transformation.world;
import io.papermc.paper.configuration.Configuration;
import io.papermc.paper.configuration.transformation.Transformations;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import static io.papermc.paper.configuration.transformation.Transformations.move;
import static org.spongepowered.configurate.NodePath.path;
import static org.spongepowered.configurate.transformation.TransformAction.remove;
public final class LegacySpigotWorldConfig {
// hunger.* keeps its paths
private static final String[] HUNGER_KEYS = {
"jump-walk-exhaustion", "jump-sprint-exhaustion", "combat-exhaustion", "regen-exhaustion",
"swim-multiplier", "sprint-multiplier", "other-multiplier"
};
private LegacySpigotWorldConfig() {
}
public static void migrate(final ConfigurationNode node, final String source) throws ConfigurateException {
final ConfigurationNode legacy = node.copy();
toNewFormat().apply(node);
node.removeChild(Configuration.LEGACY_CONFIG_VERSION_FIELD);
final Set<NodePath> keptInPlace = Arrays.stream(HUNGER_KEYS).map(key -> path("hunger", key)).collect(Collectors.toSet());
Transformations.removeUnmigrated(legacy, node, source, keptInPlace);
pruneEmptyLegacySections(node);
}
public static ConfigurationTransformation toNewFormat() {
return ConfigurationTransformation.versionedBuilder()
.versionKey(Configuration.LEGACY_CONFIG_VERSION_FIELD)
.addVersion(Configuration.FINAL_LEGACY_SPIGOT_VERSION + 1, newFormatTransformation())
.build();
}
private static ConfigurationTransformation newFormatTransformation() {
final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder()
// chunks
.addAction(path("view-distance"), move(path("chunks", "view-distance")))
.addAction(path("simulation-distance"), move(path("chunks", "simulation-distance")))
.addAction(path("unload-frozen-chunks"), move(path("chunks", "unload-frozen-chunks")))
// entities: spawning
.addAction(path("mob-spawn-range"), move(entities("spawning", "mob-spawn-range")))
.addAction(path("nerf-spawner-mobs"), move(entities("spawning", "spawner-mobs", "nerf")))
.addAction(path("enable-zombie-pigmen-portal-spawns"), move(entities("spawning", "portal-zombified-piglins", "enabled")))
.addAction(path("item-despawn-rate"), move(entities("spawning", "item-despawn-rate")))
.addAction(path("arrow-despawn-rate"), move(entities("spawning", "arrow-despawn-rate")))
.addAction(path("trident-despawn-rate"), move(entities("spawning", "trident-despawn-rate")))
// entities: behavior
.addAction(path("zombie-aggressive-towards-villager"), move(entities("behavior", "zombie-aggressive-towards-villager")))
.addAction(path("merge-radius", "item"), move(entities("behavior", "merge-radius", "item")))
.addAction(path("merge-radius", "exp"), move(entities("behavior", "merge-radius", "experience")))
// entities: tracking range (spigot uses plurals, the paper section is singular)
.addAction(path("entity-tracking-range", "players"), move(entities("tracking-range", "player")))
.addAction(path("entity-tracking-range", "animals"), move(entities("tracking-range", "animal")))
.addAction(path("entity-tracking-range", "monsters"), move(entities("tracking-range", "monster")))
.addAction(path("entity-tracking-range", "misc"), move(entities("tracking-range", "misc")))
.addAction(path("entity-tracking-range", "display"), move(entities("tracking-range", "display")))
.addAction(path("entity-tracking-range", "other"), move(entities("tracking-range", "other")))
// collisions
.addAction(path("max-entity-collisions"), move(path("collisions", "max-entity-collisions")))
// environment
.addAction(path("thunder-chance"), move(path("environment", "thunder-chance")))
// hopper
.addAction(path("hopper-amount"), move(path("hopper", "amount")))
.addAction(path("hopper-can-load-chunks"), move(path("hopper", "can-load-chunks")))
// tick rates
.addAction(path("ticks-per", "hopper-transfer"), move(path("tick-rates", "hopper-transfer")))
.addAction(path("ticks-per", "hopper-check"), move(path("tick-rates", "hopper-check")))
.addAction(path("hanging-tick-frequency"), move(path("tick-rates", "hanging-tick-frequency")))
.addAction(path("max-tick-time"), remove())
// fixes
.addAction(path("max-tnt-per-tick"), move(path("fixes", "max-tnt-per-tick")))
// misc
.addAction(path("dragon-death-sound-radius"), move(path("misc", "sound-radius", "dragon-death")))
.addAction(path("wither-spawn-sound-radius"), move(path("misc", "sound-radius", "wither-spawn")))
.addAction(path("end-portal-sound-radius"), move(path("misc", "sound-radius", "end-portal")))
// only ever controlled SpigotWorldConfig's own startup logging, which no longer exists
.addAction(path("verbose"), remove())
.addAction(path("below-zero-generation-in-existing-chunks"), remove()); // moved to the global config
// entity-activation-range.* keeps its key names, the section is just renamed
for (final String key : new String[]{
"animals", "monsters", "raiders", "misc", "water", "villagers", "flying-monsters",
"villagers-work-immunity-after", "villagers-work-immunity-for", "villagers-active-for-panic",
"tick-inactive-villagers", "ignore-spectators"
}) {
builder.addAction(path("entity-activation-range", key), move(entities("activation-range", key)));
}
for (final String category : new String[]{"animals", "monsters", "villagers", "flying-monsters"}) {
for (final String suffix : new String[]{"max-per-tick", "every", "for"}) {
final String key = category + "-" + suffix;
builder.addAction(
path("entity-activation-range", "wake-up-inactive", key),
move(entities("activation-range", "wake-up-inactive", key))
);
}
}
// growth.<plant>-modifier -> growth-modifiers.<plant>, dropping the suffix
for (final String[] pair : new String[][]{
{"cactus", "cactus"}, {"cane", "cane"}, {"melon", "melon"}, {"mushroom", "mushroom"},
{"pumpkin", "pumpkin"}, {"sapling", "sapling"}, {"beetroot", "beetroot"}, {"carrot", "carrot"},
{"potato", "potato"}, {"torchflower", "torch-flower"}, {"wheat", "wheat"}, {"netherwart", "nether-wart"},
{"vine", "vine"}, {"cocoa", "cocoa"}, {"bamboo", "bamboo"}, {"sweetberry", "sweet-berry"},
{"kelp", "kelp"}, {"twistingvines", "twisting-vines"}, {"weepingvines", "weeping-vines"},
{"cavevines", "cave-vines"}, {"glowberry", "glow-berry"}, {"pitcherplant", "pitcher-plant"}
}) {
builder.addAction(path("growth", pair[0] + "-modifier"), move(path("growth-modifiers", pair[1])));
}
// seed-<feature> -> seeds.<feature>
for (final String[] pair : new String[][]{
{"village", "village"}, {"desert", "desert"}, {"igloo", "igloo"}, {"jungle", "jungle"},
{"swamp", "swamp"}, {"monument", "monument"}, {"shipwreck", "shipwreck"}, {"ocean", "ocean"},
{"outpost", "outpost"}, {"endcity", "end-city"}, {"slime", "slime"}, {"nether", "nether"},
{"mansion", "mansion"}, {"fossil", "fossil"}, {"portal", "portal"}, {"ancientcity", "ancient-city"},
{"trailruins", "trail-ruins"}, {"trialchambers", "trial-chambers"}, {"abandonedcamp", "abandoned-camp"},
{"buriedtreasure", "buried-treasure"}, {"mineshaft", "mineshaft"}, {"stronghold", "stronghold"}
}) {
builder.addAction(path("seed-" + pair[0]), move(path("seeds", pair[1])));
}
return builder.build();
}
public static void pruneEmptyLegacySections(final ConfigurationNode node) {
for (final String legacy : new String[]{
"entity-activation-range", "entity-tracking-range", "growth", "merge-radius", "ticks-per", "max-tick-time"
}) {
final ConfigurationNode child = node.node(legacy);
if (!child.virtual() && isEffectivelyEmpty(child)) {
node.removeChild(legacy);
}
}
}
private static boolean isEffectivelyEmpty(final ConfigurationNode node) {
if (node.isMap()) {
return node.childrenMap().values().stream().allMatch(LegacySpigotWorldConfig::isEffectivelyEmpty);
}
return node.empty();
}
private static NodePath entities(final String... other) {
return Transformations.prefix("entities", other);
}
}
@@ -0,0 +1,49 @@
package io.papermc.paper.configuration.transformation.world.versioned;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import static io.papermc.paper.configuration.transformation.Transformations.move;
import static org.spongepowered.configurate.NodePath.path;
/**
* The spawn limits, spawn intervals and save interval used to defer to {@code bukkit.yml} via {@code -1},
* or {@code default} for the save interval. Move those into the Paper configs directly.
* <p>
* The nerf options join the spigot spawn toggles they belong to.
*/
public final class V32_WorldConfigCleanup {
private static final int VERSION = 32;
public static void apply(final ConfigurationTransformation.VersionedBuilder builder) {
final ConfigurationTransformation.Builder transformation = ConfigurationTransformation.builder()
.addAction(path("chunks", "auto-save-interval"), (nodePath, value) -> {
if ("default".equals(value.getString())) {
value.raw(null);
}
return null;
})
.addAction(path("entities", "behavior", "spawner-nerfed-mobs-should-jump"), move(path("entities", "spawning", "spawner-mobs", "nerfed-should-jump")))
.addAction(path("entities", "behavior", "nerf-pigmen-from-nether-portals"), move(path("entities", "spawning", "portal-zombified-piglins", "nerf")));
for (final String section : new String[]{"spawn-limits", "ticks-per-spawn"}) {
transformation.addAction(
path("entities", "spawning", section, ConfigurationTransformation.WILDCARD_OBJECT),
dropNegativeSentinel()
);
}
builder.addVersion(VERSION, transformation.build());
}
private static TransformAction dropNegativeSentinel() {
return (nodePath, value) -> {
if (value.getInt(0) < 0) {
value.raw(null);
}
return null;
};
}
private V32_WorldConfigCleanup() {
}
}
@@ -1,6 +1,5 @@
package io.papermc.paper.configuration.type.fallback;
import org.spigotmc.SpigotWorldConfig;
import org.spongepowered.configurate.serialize.SerializationException;
import java.util.Map;
@@ -24,15 +23,15 @@ public class ArrowDespawnRate extends FallbackValue.Int {
@Override
public Set<ContextKey<?>> required() {
return Set.of(FallbackValue.SPIGOT_WORLD_CONFIG);
return Set.of(FallbackValue.WORLD_CONFIG);
}
@Override
protected int fallback() {
return this.get(FallbackValue.SPIGOT_WORLD_CONFIG).arrowDespawnRate;
return this.get(FallbackValue.WORLD_CONFIG).entities.spawning.arrowDespawnRate;
}
public static ArrowDespawnRate def(SpigotWorldConfig spigotConfig) {
return new ArrowDespawnRate(FallbackValue.SPIGOT_WORLD_CONFIG.singleton(spigotConfig));
public static ArrowDespawnRate def(io.papermc.paper.configuration.WorldConfiguration worldConfig) {
return new ArrowDespawnRate(FallbackValue.WORLD_CONFIG.singleton(worldConfig));
}
}
@@ -1,39 +0,0 @@
package io.papermc.paper.configuration.type.fallback;
import net.minecraft.server.MinecraftServer;
import org.spongepowered.configurate.serialize.SerializationException;
import java.util.Map;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Supplier;
public class AutosavePeriod extends FallbackValue.Int {
AutosavePeriod(Map<ContextKey<?>, Object> contextMap, Object value) throws SerializationException {
super(contextMap, fromObject(value));
}
private AutosavePeriod(Map<ContextKey<?>, Object> contextMap) {
super(contextMap, OptionalInt.empty());
}
@Override
protected OptionalInt process(int value) {
return Util.negToDef(value);
}
@Override
protected Set<ContextKey<?>> required() {
return Set.of(FallbackValue.MINECRAFT_SERVER);
}
@Override
protected int fallback() {
return this.get(FallbackValue.MINECRAFT_SERVER).get().autosavePeriod;
}
public static AutosavePeriod def() {
return new AutosavePeriod(FallbackValue.MINECRAFT_SERVER.singleton(MinecraftServer::getServer));
}
}
@@ -1,9 +1,9 @@
package io.papermc.paper.configuration.type.fallback;
import com.google.common.base.Preconditions;
import io.papermc.paper.configuration.WorldConfiguration;
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;
@@ -16,7 +16,7 @@ import java.util.function.Supplier;
public sealed abstract class FallbackValue permits FallbackValue.Int {
private static final String DEFAULT_VALUE = "default";
static final ContextKey<SpigotWorldConfig> SPIGOT_WORLD_CONFIG = new ContextKey<>("SpigotWorldConfig");
static final ContextKey<WorldConfiguration> WORLD_CONFIG = new ContextKey<>("WorldConfiguration");
static final ContextKey<Supplier<MinecraftServer>> MINECRAFT_SERVER = new ContextKey<>("MinecraftServer");
private final Map<ContextKey<?>, Object> contextMap;
@@ -1,12 +1,12 @@
package io.papermc.paper.configuration.type.fallback;
import io.papermc.paper.configuration.WorldConfiguration;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import java.util.function.Supplier;
import net.minecraft.server.MinecraftServer;
import org.spigotmc.SpigotWorldConfig;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
@@ -18,7 +18,6 @@ public class FallbackValueSerializer extends ScalarSerializer<FallbackValue> {
static {
REGISTRY.put(ArrowDespawnRate.class, ArrowDespawnRate::new);
REGISTRY.put(AutosavePeriod.class, AutosavePeriod::new);
}
FallbackValueSerializer(Map<FallbackValue.ContextKey<?>, Object> contextMap) {
@@ -47,7 +46,7 @@ public class FallbackValueSerializer extends ScalarSerializer<FallbackValue> {
return item.serialize();
}
public static FallbackValueSerializer create(SpigotWorldConfig config, Supplier<MinecraftServer> server) {
return new FallbackValueSerializer(Map.of(FallbackValue.SPIGOT_WORLD_CONFIG, config, FallbackValue.MINECRAFT_SERVER, server));
public static FallbackValueSerializer create(WorldConfiguration config, Supplier<MinecraftServer> server) {
return new FallbackValueSerializer(Map.of(FallbackValue.WORLD_CONFIG, config, FallbackValue.MINECRAFT_SERVER, server));
}
}
@@ -0,0 +1,71 @@
package io.papermc.paper.configuration.type.number;
import com.google.common.base.Preconditions;
import com.mojang.logging.LogUtils;
import java.lang.reflect.AnnotatedType;
import java.util.OptionalLong;
import java.util.function.Function;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.spongepowered.configurate.serialize.ScalarSerializer;
public interface LongOr {
Logger LOGGER = LogUtils.getClassLogger();
default long or(final long fallback) {
return this.value().orElse(fallback);
}
OptionalLong value();
default boolean isDefined() {
return this.value().isPresent();
}
default long longValue() {
return this.value().orElseThrow();
}
record Default(OptionalLong value) implements LongOr {
public static final Default USE_DEFAULT = new Default(OptionalLong.empty());
private static final String DEFAULT_VALUE = "default";
public static final ScalarSerializer<Default> SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT);
}
final class Serializer<T extends LongOr> extends OptionalNumSerializer<T, OptionalLong> {
private Serializer(final Class<T> classOfT, final Function<OptionalLong, T> factory, final String emptySerializedValue, final T emptyValue) {
super(classOfT, emptySerializedValue, emptyValue, OptionalLong::empty, OptionalLong::isEmpty, factory, long.class);
}
@Override
protected OptionalLong full(final String value) {
return OptionalLong.of(Long.parseLong(value));
}
@Override
protected OptionalLong full(final Number num) {
if (num.longValue() != num.doubleValue()) {
LOGGER.error("{} cannot be converted to a long without losing information", num);
}
return OptionalLong.of(num.longValue());
}
@Override
protected boolean belowZero(final OptionalLong value) {
Preconditions.checkArgument(value.isPresent());
return value.getAsLong() < 0;
}
@Override
protected Object serialize(final AnnotatedType type, final T item, final Predicate<Class<?>> typeSupported) {
final OptionalLong value = item.value();
if (value.isPresent()) {
return value.getAsLong();
} else {
return this.emptySerializedValue;
}
}
}
}
@@ -0,0 +1,168 @@
/*
* Copied from GeyserMC's Configurate fork:
* https://github.com/GeyserMC/Configurate/blob/f6aa72e6099f956e78b638a2a817429007c6898a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/BlankLineStyle.java
*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.papermc.paper.configuration.yaml;
/**
* Representation of blank lines in a YAML document.
*
* @since 4.3.0
*/
public enum BlankLineStyle {
/**
* Don't do any blank line styling.
* This includes seeing blank lines during read as formatting, which will be stripped.
* To keep those blank lines, use the {@link #KEEP} style.<br>
* <br>
* This is the default.<br>
* <br>
* Example: <pre>
* {@code
* a: 1
* b:
* c: 1
* e:
* - hello
* - world
* f:
* - name: hello
* - name: world
* g:
* h:
* - name: hello
* - name: world
* i: 1
* j: 1
* }</pre>
*
* @since 4.3.0
*/
NONE,
/**
* Keep the blank lines just like they were read.<br>
* <br>
* Note that this option, unlike the others,
* will also keep read blank lines (and store it in the comment.)
* These blank lines are stored using a special character,
* so if you write to a different format you may have to remove them manually.
*
* @since 4.3.0
*/
KEEP,
/**
* Add a blank line after a nested node.<br>
* <br>
* Example: <pre>
* {@code
* a: 1
* b:
* c: 1
*
* e:
* - hello
* - world
*
* f:
* - name: hello
* - name: world
*
* g:
* h:
* - name: hello
* - name: world
*
* i: 1
*
* j: 1
* }</pre>
*
* @since 4.3.0
*/
AFTER_NESTED,
/**
* Add a blank after every root child, except the last.<br>
* <br>
* Example: <pre>
* {@code
* a: 1
*
* b:
* c: 1
*
* e:
* - hello
* - world
*
* f:
* - name: hello
* - name: world
*
* g:
* h:
* - name: hello
* - name: world
* i: 1
*
* j: 1
* }</pre>
*
* @since 4.3.0
*/
ROOT_CHILDREN,
/**
* Prepend a blank line before every node with a comment, except from the first child.<br>
* <br>
* Example: <pre>
* {@code
* # Hello
* a: 1
*
* # World
* b:
* # John
* c: 1
*
* # Doe
* e:
* - hello
* - world
*
* # Jane
* f:
* - name: hello
* - name: world
*
* # Doe
* g:
* # Lorum
* h:
* - name: hello
* - name: world
*
* # Ipsum
* i: 1
* j: 1
* }</pre>
*
* @since 4.3.0
*/
BEFORE_COMMENT
}
@@ -0,0 +1,66 @@
/*
* Copied from GeyserMC's Configurate fork:
* https://github.com/GeyserMC/Configurate/blob/f6aa72e6099f956e78b638a2a817429007c6898a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/NodeStyle.java
*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.papermc.paper.configuration.yaml;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.yaml.snakeyaml.DumperOptions;
/**
* Representation of collections and mappings in a YAML document.
*
* @since 4.0.0
*/
public enum NodeStyle {
/**
* Always use <a href="https://yaml.org/spec/1.1/#id903421">block style</a>.
*/
BLOCK(DumperOptions.FlowStyle.BLOCK),
/**
* Always use <a href="https://yaml.org/spec/1.1/#id902924">flow style</a>.
*/
FLOW(DumperOptions.FlowStyle.FLOW)
;
private final DumperOptions.FlowStyle snake;
NodeStyle(final DumperOptions.FlowStyle snake) {
this.snake = snake;
}
static DumperOptions.FlowStyle asSnakeYaml(final @Nullable NodeStyle style) {
return style == null ? DumperOptions.FlowStyle.AUTO : style.snake;
}
static @Nullable NodeStyle fromSnakeYaml(final DumperOptions.FlowStyle style) {
switch (style) {
case AUTO:
return null;
case BLOCK:
return BLOCK;
case FLOW:
return FLOW;
default:
throw new IllegalArgumentException("Unknown style " + style);
}
}
}
@@ -0,0 +1,119 @@
/*
* Copied from GeyserMC's Configurate fork:
* https://github.com/GeyserMC/Configurate/blob/f6aa72e6099f956e78b638a2a817429007c6898a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/ScalarStyle.java
*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.papermc.paper.configuration.yaml;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.yaml.snakeyaml.DumperOptions;
import java.util.EnumMap;
import java.util.Map;
/**
* Style that can be used to represent a scalar.
*
* @since 4.3.0
*/
public enum ScalarStyle {
/**
* A double-quoted string.
*
* <pre>"hello world"</pre>
*
* @since 4.3.0
*/
DOUBLE_QUOTED(DumperOptions.ScalarStyle.DOUBLE_QUOTED),
/**
* A single-quoted string.
*
* <pre>'hello world'</pre>
*
* @since 4.3.0
*/
SINGLE_QUOTED(DumperOptions.ScalarStyle.SINGLE_QUOTED),
/**
* String without any quotation.
*
* <p>This may be ambiguous with non-string types.</p>
*
* @since 4.3.0
*/
UNQUOTED(DumperOptions.ScalarStyle.PLAIN),
/**
* Folded scalar.
*
* <pre>{@code
* key: >
* folded scalar
* line breaks collapsed
* }</pre>
*
* @since 4.3.0
*/
FOLDED(DumperOptions.ScalarStyle.FOLDED),
/**
* Literal scalar.
*
* <pre>{@code
* key: |
* literal scalar
* line breaks preserved
* }</pre>
*
* @since 4.3.0
*/
LITERAL(DumperOptions.ScalarStyle.LITERAL)
;
private static final Map<DumperOptions.ScalarStyle, ScalarStyle> BY_SNAKE = new EnumMap<>(DumperOptions.ScalarStyle.class);
private final DumperOptions.ScalarStyle snake;
ScalarStyle(final DumperOptions.ScalarStyle snake) {
this.snake = snake;
}
static DumperOptions.ScalarStyle asSnakeYaml(
final @Nullable ScalarStyle style,
final DumperOptions.@Nullable ScalarStyle fallback
) {
if (style != null) {
return style.snake;
} else if (fallback != null) {
return fallback;
} else {
return DumperOptions.ScalarStyle.PLAIN;
}
}
static ScalarStyle fromSnakeYaml(final DumperOptions.ScalarStyle style) {
return BY_SNAKE.getOrDefault(style, UNQUOTED);
}
static {
for (final ScalarStyle style : values()) {
BY_SNAKE.put(style.snake, style);
}
}
}
@@ -0,0 +1,376 @@
/*
* Copied from GeyserMC's Configurate fork:
* https://github.com/GeyserMC/Configurate/blob/f6aa72e6099f956e78b638a2a817429007c6898a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlConfigurationLoader.java
*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.papermc.paper.configuration.yaml;
import net.kyori.option.Option;
import net.kyori.option.OptionSchema;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.RepresentationHint;
import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
import org.spongepowered.configurate.loader.CommentHandler;
import org.spongepowered.configurate.loader.CommentHandlers;
import org.spongepowered.configurate.util.UnmodifiableCollections;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import java.io.BufferedReader;
import java.io.Writer;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Set;
/**
* A loader for YAML-formatted configurations, using the SnakeYAML library for
* parsing and generation.
*
* @since 4.0.0
*/
public final class YamlConfigurationLoader extends AbstractConfigurationLoader<CommentedConfigurationNode> {
/**
* YAML native types from <a href="https://yaml.org/type/">YAML 1.1 Global tags</a>.
*
* <p>using SnakeYaml representation: https://bitbucket.org/snakeyaml/snakeyaml/wiki/Documentation#markdown-header-yaml-tags-and-java-types
*/
private static final Set<Class<?>> NATIVE_TYPES = UnmodifiableCollections.toSet(
Boolean.class, Integer.class, Long.class, BigInteger.class, Double.class, // numeric
byte[].class, String.class, Date.class, java.sql.Date.class, Timestamp.class); // complex types
/**
* The YAML scalar style this node should attempt to use.
*
* <p>If the chosen scalar style would produce syntactically invalid YAML, a
* valid one will replace it.</p>
*
* @since 4.3.0
*/
public static final RepresentationHint<ScalarStyle> SCALAR_STYLE = RepresentationHint.of("configurate:yaml/scalar_style", ScalarStyle.class);
/**
* The YAML node style to use for collection nodes. A {@code null} value
* will instruct the emitter to fall back to the
* {@link Builder#nodeStyle()} setting.
*
* @since 4.3.0
*/
public static final RepresentationHint<NodeStyle> NODE_STYLE = RepresentationHint.of("configurate:yaml/node_style", NodeStyle.class);
/**
* The amount of blank lines that a specific node should have.
* Absent is to inherit the {@link BlankLineStyle} of the document.
*
* @since 4.3.0
*/
public static final RepresentationHint<Integer> BLANK_LINE_STYLE_OVERRIDE
= RepresentationHint.of("configurate:yaml/blank_line_style_override", Integer.class);
/**
* Creates a new {@link YamlConfigurationLoader} builder.
*
* @return a new builder
* @since 4.0.0
*/
public static Builder builder() {
return new Builder();
}
/**
* Builds a {@link YamlConfigurationLoader}.
*
* <p>This builder supports the following options:</p>
* <dl>
* <dt>&lt;prefix&gt;.yaml.node-style</dt>
* <dd>Equivalent to {@link #nodeStyle(NodeStyle)}</dd>
* <dt>&lt;prefix&gt;.yaml.comments-enabled</dt>
* <dd>Equivalent to {@link #commentsEnabled(boolean)}</dd>
* <dt>&lt;prefix&gt;.yaml.line-length</dt>
* <dd>Equivalent to {@link #lineLength(int)}</dd>
* </dl>
*
* @since 4.0.0
*/
public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, YamlConfigurationLoader> {
private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.childSchema(AbstractConfigurationLoader.Builder.SCHEMA);
/**
* A schema of options available to configure the YAML loader.
*
* @since 4.2.0
*/
public static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView();
/**
* The collection node style to use globally when emitting with
* this loader.
*
* @see #nodeStyle(NodeStyle)
* @since 4.2.0
*/
public static final Option<NodeStyle> NODE_STYLE = UNSAFE_SCHEMA.enumOption("yaml:node_style", NodeStyle.class, null);
/**
* The indent size (in spaces) to use for documents emitted by
* the created loader.
*
* @see #indent(int)
* @since 4.2.0
*/
public static final Option<Integer> INDENT = UNSAFE_SCHEMA.intOption("yaml:indent", 4);
/**
* Whether comments are being read and written.
*
* @see #commentsEnabled(boolean)
* @since 4.3.0
*/
public static final Option<Boolean> COMMENTS_ENABLED = UNSAFE_SCHEMA.booleanOption("yaml:comments_enabled", true);
/**
* The line length before the scalar representation is split into multiple lines.
*
* @see #lineLength(int)
* @since 4.3.0
*/
public static final Option<Integer> LINE_LENGTH = UNSAFE_SCHEMA.intOption("yaml:line_length", 150);
/**
* The blank line style for writing YAML documents.
*
* @see #blankLineStyle(BlankLineStyle)
* @since 4.3.0
*/
public static final Option<BlankLineStyle> BLANK_LINE_STYLE = UNSAFE_SCHEMA.enumOption("yaml:blank_line_style", BlankLineStyle.class, null);
private final DumperOptions options = new DumperOptions();
Builder() {
this.defaultOptions(o -> o.nativeTypes(NATIVE_TYPES));
}
@Override
protected OptionSchema optionSchema() {
return SCHEMA;
}
/**
* Sets the level of indentation the resultant loader should use.
*
* @param indent the indent level
* @return this builder (for chaining)
* @since 4.0.0
*/
public Builder indent(final int indent) {
this.optionStateBuilder().value(INDENT, indent);
return this;
}
/**
* Gets the level of indentation to be used by the resultant loader.
*
* @return the indent level
* @since 4.0.0
*/
public int indent() {
return this.optionState().value(INDENT);
}
/**
* Sets the node style the built loader should use.
*
* <dl><dt>Flow</dt>
* <dd>the compact, json-like representation.<br>
* Example: <code>
* {value: [list, of, elements], another: value}
* </code></dd>
*
* <dt>Block</dt>
* <dd>expanded, traditional YAML<br>
* Example: <code>
* value:
* - list
* - of
* - elements
* another: value
* </code></dd>
* </dl>
*
* <p>A {@code null} value will tell the loader to pick a value
* automatically based on the contents of each non-scalar node.</p>
*
* @param style the node style to use
* @return this builder (for chaining)
* @since 4.0.0
*/
public Builder nodeStyle(final @Nullable NodeStyle style) {
this.optionStateBuilder().value(NODE_STYLE, style);
return this;
}
/**
* Gets the node style to be used by the resultant loader.
*
* @return the node style
* @since 4.0.0
*/
public @Nullable NodeStyle nodeStyle() {
return this.optionState().value(NODE_STYLE);
}
/**
* Sets the blank line style handling for all written files.
*
* @param style the style to apply
* @return this builder
* @see BlankLineStyle
* @since 4.3.0
*/
public Builder blankLineStyle(final @Nullable BlankLineStyle style) {
this.optionStateBuilder().value(BLANK_LINE_STYLE, style);
return this;
}
/**
* Gets the explicitly applied current blank line styling.
* This doesn't return the implicit fallback used when not setting it explicitly.
*
* @return the blank line style
* @since 4.3.0
*/
public @Nullable BlankLineStyle blankLineStyle() {
return this.optionState().value(BLANK_LINE_STYLE);
}
/**
* Set whether comment handling is enabled on this loader.
*
* <p>When comment handling is enabled, comments will be read from files
* and written back to files where possible.</p>
*
* <p>The default value is {@code true}</p>
*
* @param enableComments whether comment handling should be enabled
* @return this builder (for chaining)
* @since 4.3.0
*/
public Builder commentsEnabled(final boolean enableComments) {
this.optionStateBuilder().value(COMMENTS_ENABLED, enableComments);
return this;
}
/**
* Get whether comment handling is enabled.
*
* @return whether comment handling is enabled
* @see #commentsEnabled(boolean) for details on comment handling
* @since 4.3.0
*/
public boolean commentsEnabled() {
return this.optionState().value(COMMENTS_ENABLED);
}
/**
* Set the maximum length of a configuration line.
*
* <p>The default value is {@code 150}</p>
*
* @param lineLength the maximum length of a configuration line
* @return this builder (for chaining)
* @since 4.3.0
*/
public Builder lineLength(final int lineLength) {
this.optionStateBuilder().value(LINE_LENGTH, lineLength);
return this;
}
/**
* Get the maximum length of a configuration line.
*
* @return the maximum length of a configuration line
* @see #lineLength(int) for details on the line length
* @since 4.3.0
*/
public int lineLength() {
return this.optionState().value(LINE_LENGTH);
}
@Override
public YamlConfigurationLoader build() {
return new YamlConfigurationLoader(this);
}
}
private final LoaderOptions loaderOpts;
private final DumperOptions dumperOpts;
private final BlankLineStyle blankLineStyle;
private YamlConfigurationLoader(final Builder builder) {
super(builder, new CommentHandler[] {CommentHandlers.HASH});
this.loaderOpts = new LoaderOptions()
// .setAcceptTabs(true) - requires Configurate's patched snakeyaml
.setProcessComments(builder.commentsEnabled());
this.loaderOpts.setCodePointLimit(Integer.MAX_VALUE);
this.dumperOpts = builder.options;
this.dumperOpts.setDefaultFlowStyle(NodeStyle.asSnakeYaml(builder.optionState().value(Builder.NODE_STYLE)));
this.dumperOpts.setIndent(builder.optionState().value(Builder.INDENT));
this.dumperOpts.setProcessComments(builder.optionState().value(Builder.COMMENTS_ENABLED));
this.dumperOpts.setWidth(builder.optionState().value(Builder.LINE_LENGTH));
this.dumperOpts.setIndicatorIndent(builder.optionState().value(Builder.INDENT));
this.dumperOpts.setIndentWithIndicator(true);
final @Nullable BlankLineStyle configuredBlankLineStyle = builder.optionState().value(Builder.BLANK_LINE_STYLE);
this.blankLineStyle = configuredBlankLineStyle != null ? configuredBlankLineStyle : BlankLineStyle.NONE;
}
@Override
protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) {
final Constructor constructor = new YamlConstructor(this.loaderOpts, node.options(), this.blankLineStyle);
final Yaml yaml = new Yaml(constructor, new YamlRepresenter(this.blankLineStyle, true, this.dumperOpts), this.dumperOpts, this.loaderOpts);
@Nullable CommentedConfigurationNode loaded = yaml.load(reader);
// when a file exists but is empty (or if the file only exists of comments), the first event will be StreamEnd.
// getSingleNode will return null, getSingleData uses the Constructor of Tag Null, which just returns null.
// So we have to map null to an empty root node.
if (loaded == null) {
loaded = CommentedConfigurationNode.root(node.options());
}
node.from(loaded);
}
@Override
protected void saveInternal(final ConfigurationNode node, final Writer writer) {
final Constructor constructor = new YamlConstructor(this.loaderOpts, node.options(), this.blankLineStyle);
final Yaml yaml = new Yaml(constructor, new YamlRepresenter(this.blankLineStyle, true, this.dumperOpts), this.dumperOpts, this.loaderOpts);
yaml.dump(node, writer);
}
@Override
public CommentedConfigurationNode createNode(final ConfigurationOptions options) {
return CommentedConfigurationNode.root(options);
}
}
@@ -0,0 +1,162 @@
/*
* Copied from GeyserMC's Configurate fork:
* https://github.com/GeyserMC/Configurate/blob/f6aa72e6099f956e78b638a2a817429007c6898a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlConstructor.java
*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.papermc.paper.configuration.yaml;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.comments.CommentLine;
import org.yaml.snakeyaml.comments.CommentType;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.nodes.Tag;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
class YamlConstructor extends Constructor {
static final char BLANK_LINE_COMMENT_INDICATOR = '\0';
private static final Pattern LINE_BREAK_PATTERN = Pattern.compile("\\R");
private final ConfigurationOptions options;
private final BlankLineStyle blankLineStyle;
YamlConstructor(final LoaderOptions loadingConfig, final ConfigurationOptions options, final BlankLineStyle blankLineStyle) {
super(loadingConfig);
this.options = options;
this.blankLineStyle = blankLineStyle;
}
@Override
@EnsuresNonNull("options")
public Object getSingleData(final Class<?> type) {
return super.getSingleData(type);
}
@Override
protected Object constructObjectNoCheck(final Node yamlNode) {
final CommentedConfigurationNode node = CommentedConfigurationNode.root(this.options);
// alright, let's first check some interesting behaviour.
// When you have a file with only empty lines (has to be at least two),
// a Tag of the type COMMENT will be executed.
// It'll have a blockComment of the type empty line.
// This is unlike the behaviour of an empty file and a file with only comments,
// because both won't be seen as an object and will instead return null by Yaml#load.
// Let's filter out the multi-empty-lines behaviour and return an empty node instead.
if (yamlNode.getTag() == Tag.COMMENT) {
return node;
}
if (yamlNode.getNodeId() == NodeId.mapping) {
// make sure to mark it as a map type, even if the map itself is empty
node.raw(Collections.emptyMap());
final MappingNode mapping = (MappingNode) yamlNode;
if (mapping.getFlowStyle() != null) {
node.hint(YamlConfigurationLoader.NODE_STYLE, NodeStyle.fromSnakeYaml(mapping.getFlowStyle()));
}
for (final NodeTuple tuple : mapping.getValue()) {
final ConfigurationNode keyNode = (ConfigurationNode) this.constructObject(tuple.getKeyNode());
final Node valueNode = tuple.getValueNode();
// comments are on the key, not the value
node.node(keyNode.raw())
.from((ConfigurationNode) constructObject(valueNode))
.comment(commentFor(tuple.getKeyNode().getBlockComments()));
}
return node.comment(commentFor(yamlNode.getBlockComments()));
}
final Object raw = super.constructObjectNoCheck(yamlNode);
if (raw instanceof Collection<?>) {
// make sure to mark it as a list type, even if the collection itself is empty
node.raw(Collections.emptyList());
if (((SequenceNode) yamlNode).getFlowStyle() != null) {
node.hint(YamlConfigurationLoader.NODE_STYLE, NodeStyle.fromSnakeYaml(((SequenceNode) yamlNode).getFlowStyle()));
}
for (final Object value : (Collection<?>) raw) {
node.appendListNode().from((ConfigurationNode) value);
}
} else {
if (yamlNode instanceof ScalarNode) {
node.hint(YamlConfigurationLoader.SCALAR_STYLE, ScalarStyle.fromSnakeYaml(((ScalarNode) yamlNode).getScalarStyle()));
}
node.raw(raw);
}
return node.comment(commentFor(yamlNode.getBlockComments()));
}
private @Nullable String commentFor(final @Nullable List<CommentLine> commentLines) {
if (commentLines == null || commentLines.isEmpty()) {
return null;
}
final StringBuilder outputBuilder = new StringBuilder();
boolean first = true;
for (final CommentLine line : commentLines) {
final boolean blank = line.getCommentType() == CommentType.BLANK_LINE;
// Blank lines are considered formatting, and thus will not result in a comment.
// Users can instead configure blank lines using the BLANK_LINE_STYLE_OVERRIDE.
// Unless it's keep, as explained by its Javadoc.
if (blank && this.blankLineStyle != BlankLineStyle.KEEP) {
continue;
}
if (!first) {
outputBuilder.append(AbstractConfigurationLoader.CONFIGURATE_LINE_SEPARATOR);
}
first = false;
if (blank) {
outputBuilder.append(BLANK_LINE_COMMENT_INDICATOR);
continue;
}
final String lineStripped = removeLineBreaksForLine(line.getValue());
if (!lineStripped.isEmpty() && lineStripped.charAt(0) == ' ') {
outputBuilder.append(lineStripped, 1, lineStripped.length());
} else {
outputBuilder.append(lineStripped);
}
}
return !first ? outputBuilder.toString() : null;
}
private static String removeLineBreaksForLine(final String line) {
return LINE_BREAK_PATTERN.matcher(line).replaceAll("");
}
}
@@ -0,0 +1,197 @@
/*
* Copied from GeyserMC's Configurate fork:
* https://github.com/GeyserMC/Configurate/blob/f6aa72e6099f956e78b638a2a817429007c6898a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlRepresenter.java
*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.papermc.paper.configuration.yaml;
import static org.spongepowered.configurate.loader.AbstractConfigurationLoader.CONFIGURATE_LINE_PATTERN;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary;
import org.spongepowered.configurate.ConfigurationNode;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
import org.yaml.snakeyaml.comments.CommentLine;
import org.yaml.snakeyaml.comments.CommentType;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Represent;
import org.yaml.snakeyaml.representer.Representer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
final class YamlRepresenter extends Representer {
private static final CommentLine BLANK_LINE = new CommentLine(null, null, "", CommentType.BLANK_LINE);
private final BlankLineStyle blankLineStyle;
private final boolean padComments;
YamlRepresenter(final BlankLineStyle blankLineStyle, final boolean padComments, final DumperOptions options) {
super(options);
this.blankLineStyle = blankLineStyle;
this.padComments = padComments;
multiRepresenters.put(ConfigurationNode.class, new ConfigurationNodeRepresent());
nullRepresenter = new EmptyNullRepresenter();
}
private final class ConfigurationNodeRepresent implements Represent {
@Override
public Node representData(final Object nodeObject) {
final ConfigurationNode node = (ConfigurationNode) nodeObject;
final @Nullable Integer blankLineCount = node.ownHint(YamlConfigurationLoader.BLANK_LINE_STYLE_OVERRIDE);
final Node yamlNode;
if (node.isMap()) {
boolean first = true;
boolean previousNested = false;
final List<NodeTuple> children = new ArrayList<>();
for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) {
final ConfigurationNode child = ent.getValue();
// SnakeYAML supports both key and value comments. Add the comments on the key
final Node value = represent(child);
final Node key = represent(ent.getKey());
key.setBlockComments(value.getBlockComments());
value.setBlockComments(Collections.emptyList());
final @Nullable Integer childBlankLineCount = child.ownHint(YamlConfigurationLoader.BLANK_LINE_STYLE_OVERRIDE);
if (childBlankLineCount == null && !first && separateBefore(node, child, previousNested)) {
addBlockCommentBlankLine(key, 1);
}
first = false;
previousNested = isNested(child);
children.add(new NodeTuple(key, value));
}
yamlNode = new MappingNode(Tag.MAP, children, this.flowStyle(node));
} else if (node.isList()) {
final List<Node> children = new ArrayList<>();
for (ConfigurationNode ent : node.childrenList()) {
children.add(represent(ent));
}
yamlNode = new SequenceNode(Tag.SEQ, children, this.flowStyle(node));
} else {
final Node optionNode = represent(node.rawScalar());
final io.papermc.paper.configuration.yaml.@Nullable ScalarStyle requestedStyle
= node.ownHint(YamlConfigurationLoader.SCALAR_STYLE);
if (optionNode instanceof ScalarNode && requestedStyle != null) {
final ScalarNode scalar = (ScalarNode) optionNode;
yamlNode = new ScalarNode(
scalar.getTag(),
scalar.getValue(),
scalar.getStartMark(),
scalar.getEndMark(),
io.papermc.paper.configuration.yaml.ScalarStyle.asSnakeYaml(requestedStyle, scalar.getScalarStyle())
);
} else {
yamlNode = optionNode;
}
}
if (node instanceof CommentedConfigurationNodeIntermediary<?>) {
final @Nullable String nodeComment = ((CommentedConfigurationNodeIntermediary<?>) node).comment();
if (nodeComment != null) {
yamlNode.setBlockComments(
Arrays.stream(CONFIGURATE_LINE_PATTERN.split(nodeComment, -1))
.map(this::commentLineFor)
.collect(Collectors.toList())
);
}
}
if (blankLineCount != null && blankLineCount > 0) {
addBlockCommentBlankLine(yamlNode, blankLineCount);
}
return yamlNode;
}
private FlowStyle flowStyle(final ConfigurationNode node) {
final @Nullable NodeStyle requested = node.ownHint(YamlConfigurationLoader.NODE_STYLE);
return NodeStyle.asSnakeYaml(requested);
}
private boolean separateBefore(final ConfigurationNode parent, final ConfigurationNode child, final boolean previousNested) {
switch (YamlRepresenter.this.blankLineStyle) {
case ROOT_CHILDREN:
return parent.parent() == null;
case AFTER_NESTED:
return previousNested;
case BEFORE_COMMENT:
return child instanceof CommentedConfigurationNodeIntermediary<?>
&& ((CommentedConfigurationNodeIntermediary<?>) child).comment() != null
&& !(parent.parent() != null && parent.parent().isList());
default:
return false;
}
}
private boolean isNested(final ConfigurationNode node) {
return (node.isMap() || node.isList()) && !node.empty();
}
private void addBlockCommentBlankLine(final Node node, final int count) {
@Nullable List<CommentLine> comments = node.getBlockComments();
if (comments == null) {
comments = new ArrayList<>();
} else {
comments = new ArrayList<>(comments);
}
node.setBlockComments(comments);
comments.addAll(0, Collections.nCopies(count, BLANK_LINE));
}
private CommentLine commentLineFor(final String comment) {
if (comment.length() == 1 && comment.charAt(0) == YamlConstructor.BLANK_LINE_COMMENT_INDICATOR) {
return BLANK_LINE;
}
if (!YamlRepresenter.this.padComments || comment.isEmpty() || comment.charAt(0) == '#') {
return new CommentLine(null, null, comment, CommentType.BLOCK);
}
// prepend a space before the comment:
// before: #hello
// after: # hello
return new CommentLine(null, null, " " + comment, CommentType.BLOCK);
}
}
private static final class EmptyNullRepresenter implements Represent {
@Override
public Node representData(final Object data) {
return new ScalarNode(Tag.NULL, "", null, null, ScalarStyle.PLAIN);
}
}
}
@@ -0,0 +1,32 @@
/*
* Copied from GeyserMC's Configurate fork:
* https://github.com/GeyserMC/Configurate/blob/f6aa72e6099f956e78b638a2a817429007c6898a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/package-info.java
*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Configuration loader for the <a href="https://yaml.org/spec/1.1/">YAML 1.1</a> format.
*
* <p>While by default this loader only declares <a href="https://yaml.org/type/index.html">the YAML 1.1 global tags</a> as
* supported types, the underlying library is capable of serializing any POJO,
* and the {@link org.spongepowered.configurate.ConfigurationOptions}'s native
* types field can be adjusted to customize output.
*/
@DefaultQualifier(NonNull.class)
package io.papermc.paper.configuration.yaml;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
@@ -1,7 +1,7 @@
package io.papermc.paper.plugin;
import com.mojang.logging.LogUtils;
import io.papermc.paper.configuration.PaperConfigurations;
import io.papermc.paper.configuration.PaperConfigurationsInitializer;
import io.papermc.paper.plugin.entrypoint.Entrypoint;
import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
import io.papermc.paper.plugin.provider.PluginProvider;
@@ -11,7 +11,6 @@ import java.util.Set;
import java.util.TreeSet;
import joptsimple.OptionSet;
import net.minecraft.server.dedicated.DedicatedServer;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -34,14 +33,11 @@ public class PluginInitializerManager {
}
private static PluginInitializerManager parse(@NotNull final OptionSet minecraftOptionSet) throws Exception {
// We have to load the bukkit configuration inorder to get the update folder location.
final File configFileLocationBukkit = (File) minecraftOptionSet.valueOf("bukkit-settings");
final Path pluginDirectory = ((File) minecraftOptionSet.valueOf("plugins")).toPath();
final YamlConfiguration configuration = PaperConfigurations.loadLegacyConfigFile(configFileLocationBukkit);
final String updateDirectoryName = configuration.getString("settings.update-folder", "update");
// This runs before the registries are bootstrapped, so the full global config cannot be read here.
// The update folder comes from the early initialization pass instead.
final String updateDirectoryName = PaperConfigurationsInitializer.earlyGlobalConfig().misc.updateFolder;
if (updateDirectoryName.isBlank()) {
return new PluginInitializerManager(pluginDirectory, null);
}
@@ -50,7 +46,7 @@ public class PluginInitializerManager {
if (!Files.isDirectory(resolvedUpdateDirectory)) {
if (Files.exists(resolvedUpdateDirectory)) {
LOGGER.error("Misconfigured update directory!");
LOGGER.error("Your configured update directory ({}) in bukkit.yml is pointing to a non-directory path. " +
LOGGER.error("Your configured update directory ({}) in initialization.update-folder is pointing to a non-directory path. " +
"Auto updating functionality will not work.", resolvedUpdateDirectory);
}
return new PluginInitializerManager(pluginDirectory, null);
@@ -67,7 +63,7 @@ public class PluginInitializerManager {
if (isSameFile) {
LOGGER.error("Misconfigured update directory!");
LOGGER.error(("Your configured update directory (%s) in bukkit.yml is pointing to the same location as the plugin directory (%s). " +
LOGGER.error(("Your configured update directory (%s) in initialization.update-folder is pointing to the same location as the plugin directory (%s). " +
"Disabling auto updating functionality.").formatted(resolvedUpdateDirectory, pluginDirectory));
return new PluginInitializerManager(pluginDirectory, null);
@@ -97,7 +93,6 @@ public class PluginInitializerManager {
public static void load(OptionSet optionSet) throws Exception {
LOGGER.info("Initializing plugins...");
// We have to load the bukkit configuration inorder to get the update folder location.
io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionSet);
// Register the default plugin directory
@@ -5,6 +5,8 @@ import com.google.common.collect.ImmutableList;
import io.papermc.paper.configuration.constraint.Constraint;
import io.papermc.paper.configuration.serializer.ComponentSerializer;
import io.papermc.paper.configuration.serializer.EnumValueSerializer;
import io.papermc.paper.configuration.yaml.NodeStyle;
import io.papermc.paper.configuration.yaml.YamlConfigurationLoader;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer;
import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
@@ -28,8 +30,6 @@ import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.objectmapping.meta.Required;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.io.BufferedReader;
import java.util.EnumMap;
@@ -103,7 +103,7 @@ public final class PaperFilledProfileCache {
}
private int maxSize() {
return org.spigotmc.SpigotConfig.userCacheCap;
return io.papermc.paper.configuration.GlobalConfiguration.get().players.userCacheSize;
}
private boolean tryCancelCleanup() {
@@ -191,7 +191,7 @@ public class CraftChunk implements Chunk {
@Override
public boolean isSlimeChunk() {
// 987234911L is taken from Slime when seeing if a slime can spawn in a chunk
return this.level.paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), level.spigotConfig.slimeSeed).nextInt(10) == 0; // Paper
return this.level.paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), level.paperConfig().seeds.slime).nextInt(10) == 0; // Paper
}
@Override
@@ -10,7 +10,9 @@ import com.google.common.collect.MapMaker;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.serialization.JsonOps;
import io.papermc.paper.adventure.PaperAdventure;
import io.papermc.paper.configuration.GlobalConfiguration;
import io.papermc.paper.configuration.PaperConfigurations;
import io.papermc.paper.configuration.PaperServerConfiguration;
import io.papermc.paper.configuration.ServerConfiguration;
import io.papermc.paper.util.MCUtil;
@@ -45,6 +47,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.minecraft.Optionull;
import net.minecraft.advancements.AdvancementHolder;
@@ -80,6 +83,7 @@ import net.minecraft.util.Util;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.world.damagesource.DamageType;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.entity.ai.village.VillageSiege;
import net.minecraft.world.entity.npc.CatSpawner;
import net.minecraft.world.entity.npc.wanderingtrader.WanderingTraderSpawner;
@@ -258,14 +262,12 @@ public final class CraftServer implements Server {
final DedicatedServer console;
private final DedicatedPlayerList playerList;
private final Map<String, World> worlds = new LinkedHashMap<>();
private YamlConfiguration configuration;
private YamlConfiguration commandsConfiguration;
private final Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
private final Map<UUID, OfflinePlayer> offlinePlayers = new MapMaker().weakValues().makeMap();
private final EntityMetadataStore entityMetadata = new EntityMetadataStore();
private final PlayerMetadataStore playerMetadata = new PlayerMetadataStore();
private final WorldMetadataStore worldMetadata = new WorldMetadataStore();
private final Object2IntOpenHashMap<SpawnCategory> spawnCategoryLimit = new Object2IntOpenHashMap<>();
private File container;
private WarningState warningState = WarningState.DEFAULT;
public ApiVersion minimumAPI;
@@ -399,20 +401,6 @@ public final class CraftServer implements Server {
this.getLogger().info("Console input is disabled due to --noconsole command argument");
}
this.configuration = YamlConfiguration.loadConfiguration(this.getConfigFile());
this.configuration.options().copyDefaults(true);
YamlConfiguration configurationDefaults = YamlConfiguration.loadConfiguration(new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream("configurations/bukkit.yml"), StandardCharsets.UTF_8));
this.configuration.setDefaults(configurationDefaults);
this.configuration.options().setHeader(configurationDefaults.options().getHeader());
ConfigurationSection legacyAlias = null;
if (!this.configuration.isString("aliases")) {
legacyAlias = this.configuration.getConfigurationSection("aliases");
this.configuration.set("aliases", "now-in-commands.yml");
}
this.saveConfig();
if (this.getCommandsConfigFile().isFile()) {
legacyAlias = null;
}
this.commandsConfiguration = YamlConfiguration.loadConfiguration(this.getCommandsConfigFile());
this.commandsConfiguration.options().copyDefaults(true);
// Paper start - don't enforce icanhasbukkit default if alias block exists
@@ -421,40 +409,16 @@ public final class CraftServer implements Server {
this.commandsConfiguration.setDefaults(commandsDefaults);
// Paper end - don't enforce icanhasbukkit default if alias block exists
this.commandsConfiguration.options().setHeader(commandsDefaults.options().getHeader());
this.saveCommandsConfig();
// Migrate aliases from old file and add previously implicit $1- to pass all arguments
if (legacyAlias != null) {
ConfigurationSection aliases = this.commandsConfiguration.createSection("aliases");
for (String key : legacyAlias.getKeys(false)) {
List<String> commands = new ArrayList<>();
if (legacyAlias.isList(key)) {
for (String command : legacyAlias.getStringList(key)) {
commands.add(command + " $1-");
}
} else {
commands.add(legacyAlias.getString(key) + " $1-");
}
aliases.set(key, commands);
}
}
this.saveCommandsConfig();
this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*");
this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions");
this.overrideSpawnLimits();
console.autosavePeriod = this.configuration.getInt("ticks-per.autosave");
this.warningState = WarningState.value(this.configuration.getString("settings.deprecated-verbose"));
TicketType.PLUGIN_TYPE_TIMEOUT = this.configuration.getInt("chunk-gc.period-in-ticks");
this.minimumAPI = ApiVersion.getOrCreateVersion(this.configuration.getString("settings.minimum-api"));
this.applyPaperConfiguration(); // Paper - moved from bukkit.yml
this.loadIcon();
this.loadCompatibilities();
CraftMagicNumbers.INSTANCE.getCommodore().updateReroute(activeCompatibilities::contains);
// Set map color cache
if (this.configuration.getBoolean("settings.use-map-color-cache")) {
if (GlobalConfiguration.get().misc.useMapColorCache) {
MapPalette.setMapColorCache(new CraftMapColorCache(this.logger));
}
this.potionBrewer = new io.papermc.paper.potion.PaperPotionBrewer(console); // Paper - custom potion mixes
@@ -462,32 +426,35 @@ public final class CraftServer implements Server {
this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark
}
public boolean getCommandBlockOverride(String command) {
return this.overrideAllCommandBlockCommands || this.commandsConfiguration.getStringList("command-block-overrides").contains(command);
}
private File getConfigFile() {
return (File) this.console.options.valueOf("bukkit-settings");
}
private File getCommandsConfigFile() {
return (File) this.console.options.valueOf("commands-settings");
}
private void overrideSpawnLimits() {
for (SpawnCategory spawnCategory : SpawnCategory.values()) {
if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
this.spawnCategoryLimit.put(spawnCategory, this.configuration.getInt(CraftSpawnCategory.getConfigNameSpawnLimit(spawnCategory)));
public void applyPaperConfiguration() {
final GlobalConfiguration config = GlobalConfiguration.get();
this.console.autosavePeriod = config.misc.autoSaveInterval;
this.warningState = config.misc.deprecatedVerbose;
TicketType.PLUGIN_TYPE_TIMEOUT = config.chunkSystem.pluginTicketTimeout;
this.minimumAPI = ApiVersion.getOrCreateVersion(config.misc.minimumApi);
for (final ServerLevel world : this.console.getAllLevels()) {
for (final SpawnCategory spawnCategory : SpawnCategory.values()) {
if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
final MobCategory category = CraftSpawnCategory.toNMS(spawnCategory);
world.ticksPerSpawnCategory.put(spawnCategory, world.paperConfig().entities.spawning.ticksPerSpawn.getInt(category));
world.getWorld().setSpawnLimit(spawnCategory, world.paperConfig().entities.spawning.spawnLimits.getInt(category));
}
}
}
}
private void saveConfig() {
try {
this.configuration.save(this.getConfigFile());
} catch (IOException ex) {
Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, "Could not save " + this.getConfigFile(), ex);
}
public void reloadPaperConfiguration() {
this.console.paperConfigurations.reloadConfigs(this.console);
this.applyPaperConfiguration();
this.syncCommands();
}
public boolean getCommandBlockOverride(String command) {
return this.overrideAllCommandBlockCommands || this.commandsConfiguration.getStringList("command-block-overrides").contains(command);
}
private File getCommandsConfigFile() {
return (File) this.console.options.valueOf("commands-settings");
}
private void saveCommandsConfig() {
@@ -498,33 +465,11 @@ public final class CraftServer implements Server {
}
}
private void loadCompatibilities() {
if (true) return; // Paper - Big nope
ConfigurationSection compatibilities = this.configuration.getConfigurationSection("settings.compatibility");
if (compatibilities == null) {
this.activeCompatibilities = Collections.emptySet();
return;
}
this.activeCompatibilities = compatibilities
.getKeys(false)
.stream()
.filter(compatibilities::getBoolean)
.collect(Collectors.toSet());
if (!this.activeCompatibilities.isEmpty()) {
this.logger.info("Using following compatibilities: `" + Joiner.on("`, `").join(this.activeCompatibilities) + "`, this will affect performance and other plugins behavior.");
this.logger.info("Only use when necessary and prefer updating plugins if possible.");
}
if (this.activeCompatibilities.contains("enum-compatibility-mode")) {
this.getLogger().warning("Loading plugins in enum compatibility mode. This will affect plugin performance. Use only as a transition period or when absolutely necessary.");
} else if (System.getProperty("RemoveEnumBanner") == null) {
// TODO 2024-06-16: Remove in newer version
this.getLogger().info("*** This version of Spigot contains changes to some enums. If you notice that plugins no longer work after updating, please report this to the developers of those plugins first. ***");
this.getLogger().info("*** If you cannot update those plugins, you can try setting `settings.compatibility.enum-compatibility-mode` to `true` in `bukkit.yml`. ***");
}
}
public void loadPlugins() {
io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation
@@ -561,7 +506,7 @@ public final class CraftServer implements Server {
if (type == PluginLoadOrder.STARTUP) {
this.helpMap.clear();
this.helpMap.initializeGeneralTopics();
if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions(); // Paper
if (GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions(); // Paper
}
Plugin[] plugins = this.pluginManager.getPlugins();
@@ -578,7 +523,7 @@ public final class CraftServer implements Server {
// Spigot end
DefaultPermissions.registerCorePermissions();
CraftDefaultPermissions.registerCorePermissions();
if (!io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) this.loadCustomPermissions(); // Paper
if (!GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) this.loadCustomPermissions(); // Paper
this.syncCommands();
}
}
@@ -764,7 +709,7 @@ public final class CraftServer implements Server {
@Override
public boolean getAllowEnd() {
return this.configuration.getBoolean("settings.allow-end");
return GlobalConfiguration.get().misc.enableEnd;
}
@Override
@@ -778,11 +723,11 @@ public final class CraftServer implements Server {
}
public boolean getWarnOnOverload() {
return this.configuration.getBoolean("settings.warn-on-overload");
return true; // Paper - option removed, nothing internal read it
}
public boolean getQueryPlugins() {
return this.configuration.getBoolean("settings.query-plugins");
return GlobalConfiguration.get().misc.queryPlugins;
}
@Override
@@ -838,21 +783,21 @@ public final class CraftServer implements Server {
@Override
public String getUpdateFolder() {
return this.configuration.getString("settings.update-folder", "update");
return GlobalConfiguration.get().misc.updateFolder;
}
@Override
public File getUpdateFolderFile() {
return new File((File) this.console.options.valueOf("plugins"), this.configuration.getString("settings.update-folder", "update"));
return new File((File) this.console.options.valueOf("plugins"), GlobalConfiguration.get().misc.updateFolder);
}
@Override
public long getConnectionThrottle() {
// Spigot start - Automatically set connection throttle for bungee configurations
if (org.spigotmc.SpigotConfig.bungee || io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) { // Paper - Add Velocity IP Forwarding Support
if (GlobalConfiguration.get().proxies.bungeeCord.enabled || GlobalConfiguration.get().proxies.velocity.enabled) { // Paper - Add Velocity IP Forwarding Support
return -1;
} else {
return this.configuration.getInt("settings.connection-throttle");
return GlobalConfiguration.get().misc.connectionThrottle;
}
// Spigot end
}
@@ -861,7 +806,11 @@ public final class CraftServer implements Server {
public int getTicksPerSpawns(SpawnCategory spawnCategory) {
Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null");
Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory);
return this.configuration.getInt(CraftSpawnCategory.getConfigNameTicksPerSpawn(spawnCategory));
// Paper - spawn intervals are per world; report the primary world's
final List<World> worlds = this.getWorlds();
return worlds.isEmpty()
? (int) CraftSpawnCategory.getDefaultTicksPerSpawn(spawnCategory)
: (int) worlds.get(0).getTicksPerSpawns(spawnCategory);
}
@Override
@@ -936,21 +885,14 @@ public final class CraftServer implements Server {
// Paper end - lifecycle events
org.spigotmc.WatchdogThread.hasStarted = false; // Paper - Disable watchdog early timeout on reload
this.reloadCount++;
this.configuration = YamlConfiguration.loadConfiguration(this.getConfigFile());
this.commandsConfiguration = YamlConfiguration.loadConfiguration(this.getCommandsConfigFile());
this.console.settings = new DedicatedServerSettings(this.console.options);
DedicatedServerProperties config = this.console.settings.getProperties();
this.console.setMotd(config.motd.get());
this.overrideSpawnLimits();
this.warningState = WarningState.value(this.configuration.getString("settings.deprecated-verbose"));
TicketType.PLUGIN_TYPE_TIMEOUT = this.configuration.getInt("chunk-gc.period-in-ticks");
this.minimumAPI = ApiVersion.getOrCreateVersion(this.configuration.getString("settings.minimum-api"));
this.printSaveWarning = false;
this.console.autosavePeriod = this.configuration.getInt("ticks-per.autosave");
this.loadIcon();
this.loadCompatibilities();
CraftMagicNumbers.INSTANCE.getCommodore().updateReroute(activeCompatibilities::contains);
try {
@@ -964,23 +906,11 @@ public final class CraftServer implements Server {
this.logger.log(Level.WARNING, "Failed to load banned-players.json, " + ex.getMessage());
}
org.spigotmc.SpigotConfig.init((File) this.console.options.valueOf("spigot-settings")); // Spigot
this.console.paperConfigurations.reloadConfigs(this.console);
this.applyPaperConfiguration();
for (ServerLevel world : this.console.getAllLevels()) {
// world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty
world.setSpawnSettings(world.isSpawningMonsters()); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean))
for (SpawnCategory spawnCategory : SpawnCategory.values()) {
if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
long ticksPerCategorySpawn = this.getTicksPerSpawns(spawnCategory);
if (ticksPerCategorySpawn < 0) {
world.ticksPerSpawnCategory.put(spawnCategory, CraftSpawnCategory.getDefaultTicksPerSpawn(spawnCategory));
} else {
world.ticksPerSpawnCategory.put(spawnCategory, ticksPerCategorySpawn);
}
}
}
world.spigotConfig.init(); // Spigot
}
Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper
@@ -995,7 +925,6 @@ public final class CraftServer implements Server {
}
// Paper end
this.reloadData();
org.spigotmc.SpigotConfig.registerCommands(); // Spigot
io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper
this.spark.registerCommandBeforePlugins(this); // Paper - spark
this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*");
@@ -1093,7 +1022,7 @@ public final class CraftServer implements Server {
}
private void loadCustomPermissions() {
File file = new File(this.configuration.getString("settings.permissions-file"));
File file = new File(GlobalConfiguration.get().misc.permissionsFile);
if (!file.isFile()) {
return;
}
@@ -1683,7 +1612,7 @@ public final class CraftServer implements Server {
@Override
@Deprecated // Paper
public String getShutdownMessage() {
return this.configuration.getString("settings.shutdown-message");
return CraftChatMessage.fromComponent(PaperAdventure.asVanilla(GlobalConfiguration.get().messages.shutdown));
}
@Override
@@ -1732,14 +1661,12 @@ public final class CraftServer implements Server {
}
public ChunkGenerator getGenerator(String world) {
ConfigurationSection section = this.configuration.getConfigurationSection("worlds");
final GlobalConfiguration.WorldGenerators generators = GlobalConfiguration.get().worldGenerators.get(world); // Paper - moved from bukkit.yml
ChunkGenerator result = null;
if (section != null) {
section = section.getConfigurationSection(world);
if (section != null) {
String name = section.getString("generator");
if (generators != null) {
{
String name = generators.generator();
if (name != null && !name.isEmpty()) {
String[] split = name.split(":", 2);
@@ -1768,14 +1695,12 @@ public final class CraftServer implements Server {
}
public BiomeProvider getBiomeProvider(String world) {
ConfigurationSection section = this.configuration.getConfigurationSection("worlds");
GlobalConfiguration.WorldGenerators generators = GlobalConfiguration.get().worldGenerators.get(world); // Paper - moved from bukkit.yml
BiomeProvider result = null;
if (section != null) {
section = section.getConfigurationSection(world);
if (section != null) {
String name = section.getString("biome-provider");
if (generators != null) {
{
String name = generators.biomeProvider();
if (name != null && !name.isEmpty()) {
String[] split = name.split(":", 2);
@@ -1909,7 +1834,7 @@ public final class CraftServer implements Server {
}
NameAndId nameAndId;
// Only fetch an online UUID in online mode
if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) {
if (GlobalConfiguration.get().proxies.isProxyOnlineMode()) {
nameAndId = console.services().nameToIdCache().get(name).orElse(null);
} else {
// Make an OfflinePlayer using an offline mode UUID since the name has no profile
@@ -1927,7 +1852,7 @@ public final class CraftServer implements Server {
if (result == null) {
NameAndId profile = null;
// Only fetch an online UUID in online mode
if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) { // Paper - Add setting for proxy online mode status
if (GlobalConfiguration.get().proxies.isProxyOnlineMode()) { // Paper - Add setting for proxy online mode status
// This is potentially blocking :(
profile = this.console.services().nameToIdCache().get(name).orElse(null);
}
@@ -2288,11 +2213,8 @@ public final class CraftServer implements Server {
public int getSpawnLimit(SpawnCategory spawnCategory) {
Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null");
Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory." + spawnCategory + " does not have a spawn limit.");
return this.getSpawnLimitUnsafe(spawnCategory);
}
public int getSpawnLimitUnsafe(final SpawnCategory spawnCategory) {
return this.spawnCategoryLimit.getOrDefault(spawnCategory, -1);
final List<World> worlds = this.getWorlds();
return worlds.isEmpty() ? -1 : worlds.get(0).getSpawnLimit(spawnCategory);
}
@Override
@@ -2695,27 +2617,6 @@ public final class CraftServer implements Server {
private final org.bukkit.Server.Spigot spigot = new org.bukkit.Server.Spigot() {
@Deprecated
@Override
public YamlConfiguration getConfig() {
return org.spigotmc.SpigotConfig.config;
}
@Override
public YamlConfiguration getBukkitConfig() {
return configuration;
}
@Override
public YamlConfiguration getSpigotConfig() {
return org.spigotmc.SpigotConfig.config;
}
@Override
public YamlConfiguration getPaperConfig() {
return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console);
}
@Override
public void restart() {
CraftServer.this.restart();
@@ -2832,7 +2733,7 @@ public final class CraftServer implements Server {
@Override
public void reloadPermissions() {
pluginManager.clearPermissions();
if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions();
if (GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions();
for (Plugin plugin : pluginManager.getPlugins()) {
for (Permission perm : plugin.getDescription().getPermissions()) {
try {
@@ -2842,7 +2743,7 @@ public final class CraftServer implements Server {
}
}
}
if (!io.papermc.paper.configuration.GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions();
if (!GlobalConfiguration.get().misc.loadPermissionsYmlBeforePlugins) loadCustomPermissions();
DefaultPermissions.registerCorePermissions();
CraftDefaultPermissions.registerCorePermissions();
}
@@ -2868,17 +2769,17 @@ public final class CraftServer implements Server {
@Override
public boolean suggestPlayerNamesWhenNullTabCompletions() {
return io.papermc.paper.configuration.GlobalConfiguration.get().commands.suggestPlayerNamesWhenNullTabCompletions;
return GlobalConfiguration.get().commands.suggestPlayerNamesWhenNullTabCompletions;
}
@Override
public String getPermissionMessage() {
return net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacyAmpersand().serialize(io.papermc.paper.configuration.GlobalConfiguration.get().messages.noPermission);
return LegacyComponentSerializer.legacyAmpersand().serialize(GlobalConfiguration.get().messages.noPermission);
}
@Override
public net.kyori.adventure.text.Component permissionMessage() {
return io.papermc.paper.configuration.GlobalConfiguration.get().messages.noPermission;
return GlobalConfiguration.get().messages.noPermission;
}
@Override
@@ -1545,11 +1545,14 @@ public class CraftWorld extends CraftRegionAccessor implements World {
}
public final int getSpawnLimitUnsafe(final SpawnCategory spawnCategory) {
int limit = this.spawnCategoryLimit.getOrDefault(spawnCategory, -1);
if (limit < 0) {
limit = this.server.getSpawnLimitUnsafe(spawnCategory);
final int limit = this.spawnCategoryLimit.getOrDefault(spawnCategory, -1);
if (limit >= 0) {
return limit;
}
return limit;
final net.minecraft.world.entity.MobCategory category = CraftSpawnCategory.toNMS(spawnCategory);
final int configured = this.world.paperConfig().entities.spawning.spawnLimits.getInt(category);
return configured >= 0 ? configured : category.getMaxInstancesPerChunk();
}
@Override
@@ -768,7 +768,7 @@ public class CraftEventFactory {
// Spigot start - SPIGOT-7523: Merge after spawn event and only merge if the event was not cancelled (gets checked above)
if (entity instanceof net.minecraft.world.entity.ExperienceOrb xp) {
double radius = level.spigotConfig.expMerge;
double radius = level.paperConfig().entities.behavior.mergeRadius.experience;
if (radius > 0) {
// Paper start - Maximum exp value when merging; Whole section has been tweaked, see comments for specifics
final long maxValue = level.paperConfig().entities.behavior.experienceMergeMaxValue;
@@ -26,7 +26,7 @@ public class RestartCommand extends Command {
}
public static void restart() {
RestartCommand.restart(SpigotConfig.restartScript);
RestartCommand.restart(io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.restartScript);
}
private static void restart(final String restartScript) {
@@ -34,9 +34,9 @@ public class RestartCommand extends Command {
// Paper - extract method and cleanup
boolean isRestarting = addShutdownHook(restartScript);
if (isRestarting) {
System.out.println("Attempting to restart with " + SpigotConfig.restartScript);
System.out.println("Attempting to restart with " + io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.restartScript);
} else {
System.out.println("Startup script '" + SpigotConfig.restartScript + "' does not exist! Stopping server.");
System.out.println("Startup script '" + io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.restartScript + "' does not exist! Stopping server.");
}
// Stop the watchdog
WatchdogThread.doStop();
@@ -53,7 +53,7 @@ public class RestartCommand extends Command {
if (MinecraftServer.getServer().isSameThread()) {
// Kick all players
for (ServerPlayer p : com.google.common.collect.ImmutableList.copyOf(MinecraftServer.getServer().getPlayerList().getPlayers())) {
p.connection.disconnect(CraftChatMessage.fromStringOrEmpty(SpigotConfig.restartMessage, true), org.bukkit.event.player.PlayerKickEvent.Cause.RESTART_COMMAND); // Paper - kick event reason (cause is never used)
p.connection.disconnect(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.restart, org.bukkit.event.player.PlayerKickEvent.Cause.RESTART_COMMAND); // Paper - kick event reason (cause is never used)
}
// Give the socket a chance to send the packets
try {
@@ -1,50 +0,0 @@
package org.spigotmc;
import java.io.File;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import static net.kyori.adventure.text.Component.text;
public class SpigotCommand extends Command {
public SpigotCommand(String name) {
super(name);
this.description = "Spigot related commands";
this.usageMessage = "/spigot reload";
this.setPermission("bukkit.command.spigot");
}
@Override
public boolean execute(CommandSender sender, String commandLabel, String[] args) {
if (!this.testPermission(sender)) return true;
if (args.length != 1 || !args[0].equals("reload")) {
sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED));
return false;
}
Command.broadcastCommandMessage(sender, text().color(NamedTextColor.RED)
.append(text("Please note that this command is not supported and may cause issues."))
.appendNewline()
.append(text("If you encounter any issues please use the /stop command to restart your server."))
.build()
);
MinecraftServer console = MinecraftServer.getServer();
org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings"));
for (ServerLevel world : console.getAllLevels()) {
world.spigotConfig.init();
}
console.server.reloadCount++;
Command.broadcastCommandMessage(sender, text("Reload complete.", NamedTextColor.GREEN));
return true;
}
}
@@ -1,355 +0,0 @@
package org.spigotmc;
import com.google.common.base.Throwables;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.attributes.RangedAttribute;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
public class SpigotConfig {
private static File CONFIG_FILE;
private static final String HEADER = """
This is the Spigot configuration file for Paper.
As you can see, there's tons 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.
File Reference: https://docs.papermc.io/paper/reference/spigot-configuration/
Docs: https://docs.papermc.io/
Discord: https://discord.gg/papermc
Website: https://papermc.io/
""";
/*========================================================================*/
public static YamlConfiguration config;
static int version;
static Map<String, Command> commands;
/*========================================================================*/
public static void init(File configFile) {
SpigotConfig.CONFIG_FILE = configFile;
SpigotConfig.config = new YamlConfiguration();
try {
SpigotConfig.config.load(SpigotConfig.CONFIG_FILE);
} catch (IOException ignored) {
} catch (InvalidConfigurationException ex) {
Bukkit.getLogger().log(Level.SEVERE, "Could not load spigot.yml, please correct your syntax errors", ex);
throw Throwables.propagate(ex);
}
SpigotConfig.config.options().header(SpigotConfig.HEADER);
SpigotConfig.config.options().copyDefaults(true);
SpigotConfig.commands = new HashMap<>();
SpigotConfig.commands.put("spigot", new SpigotCommand("spigot"));
SpigotConfig.version = SpigotConfig.getInt("config-version", 13);
SpigotConfig.set("config-version", 13);
SpigotConfig.readConfig(SpigotConfig.class, null);
}
public static void registerCommands() {
for (Map.Entry<String, Command> entry : SpigotConfig.commands.entrySet()) {
MinecraftServer.getServer().server.getCommandMap().register(entry.getKey(), "Spigot", entry.getValue());
}
}
public static void readConfig(Class<?> clazz, Object instance) {
for (Method method : clazz.getDeclaredMethods()) {
if (Modifier.isPrivate(method.getModifiers())) {
if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) {
try {
method.setAccessible(true);
method.invoke(instance);
} catch (InvocationTargetException ex) {
throw Throwables.propagate(ex.getCause());
} catch (Exception ex) {
Bukkit.getLogger().log(Level.SEVERE, "Error invoking " + method, ex);
}
}
}
}
try {
SpigotConfig.config.save(SpigotConfig.CONFIG_FILE);
} catch (IOException ex) {
Bukkit.getLogger().log(Level.SEVERE, "Could not save " + SpigotConfig.CONFIG_FILE, ex);
}
}
private static void set(String path, Object val) {
SpigotConfig.config.set(path, val);
}
private static boolean getBoolean(String path, boolean def) {
SpigotConfig.config.addDefault(path, def);
return SpigotConfig.config.getBoolean(path, SpigotConfig.config.getBoolean(path));
}
private static int getInt(String path, int def) {
SpigotConfig.config.addDefault(path, def);
return SpigotConfig.config.getInt(path, SpigotConfig.config.getInt(path));
}
private static <T> List getList(String path, T def) {
SpigotConfig.config.addDefault(path, def);
return (List<T>) SpigotConfig.config.getList(path, SpigotConfig.config.getList(path));
}
private static String getString(String path, String def) {
SpigotConfig.config.addDefault(path, def);
return SpigotConfig.config.getString(path, SpigotConfig.config.getString(path));
}
private static double getDouble(String path, double def) {
SpigotConfig.config.addDefault(path, def);
return SpigotConfig.config.getDouble(path, SpigotConfig.config.getDouble(path));
}
public static boolean logCommands;
private static void logCommands() {
SpigotConfig.logCommands = SpigotConfig.getBoolean("commands.log", true);
}
public static int tabComplete;
public static boolean sendNamespaced;
private static void tabComplete() {
if (SpigotConfig.version < 6) {
boolean oldValue = SpigotConfig.getBoolean("commands.tab-complete", true);
if (oldValue) {
SpigotConfig.set("commands.tab-complete", 0);
} else {
SpigotConfig.set("commands.tab-complete", -1);
}
}
SpigotConfig.tabComplete = SpigotConfig.getInt("commands.tab-complete", 0);
SpigotConfig.sendNamespaced = SpigotConfig.getBoolean("commands.send-namespaced", true);
}
public static String whitelistMessage;
public static String unknownCommandMessage;
public static String serverFullMessage;
public static String outdatedClientMessage = "Outdated client! Please use {0}";
public static String outdatedServerMessage = "Outdated server! I'm still on {0}";
private static String transform(String s) {
return ChatColor.translateAlternateColorCodes('&', s).replaceAll("\\\\n", "\n");
}
private static void messages() {
if (SpigotConfig.version < 8) {
SpigotConfig.set("messages.outdated-client", SpigotConfig.outdatedClientMessage);
SpigotConfig.set("messages.outdated-server", SpigotConfig.outdatedServerMessage);
}
SpigotConfig.whitelistMessage = SpigotConfig.transform(SpigotConfig.getString("messages.whitelist", "You are not whitelisted on this server!"));
SpigotConfig.unknownCommandMessage = SpigotConfig.transform(SpigotConfig.getString("messages.unknown-command", "Unknown command. Type \"/help\" for help."));
SpigotConfig.serverFullMessage = SpigotConfig.transform(SpigotConfig.getString("messages.server-full", "The server is full!"));
SpigotConfig.outdatedClientMessage = SpigotConfig.transform(SpigotConfig.getString("messages.outdated-client", SpigotConfig.outdatedClientMessage));
SpigotConfig.outdatedServerMessage = SpigotConfig.transform(SpigotConfig.getString("messages.outdated-server", SpigotConfig.outdatedServerMessage));
}
public static int timeoutTime = 60;
public static boolean restartOnCrash = true;
public static String restartScript = "./start.sh";
public static String restartMessage;
private static void watchdog() {
SpigotConfig.timeoutTime = SpigotConfig.getInt("settings.timeout-time", SpigotConfig.timeoutTime);
SpigotConfig.restartOnCrash = SpigotConfig.getBoolean("settings.restart-on-crash", SpigotConfig.restartOnCrash);
SpigotConfig.restartScript = SpigotConfig.getString("settings.restart-script", SpigotConfig.restartScript);
SpigotConfig.restartMessage = SpigotConfig.transform(SpigotConfig.getString("messages.restart", "Server is restarting"));
SpigotConfig.commands.put("restart", new RestartCommand("restart"));
}
public static boolean bungee;
private static void bungee() {
if (SpigotConfig.version < 4) {
SpigotConfig.set("settings.bungeecord", false);
System.out.println("Outdated config, disabling BungeeCord support!");
}
SpigotConfig.bungee = SpigotConfig.getBoolean("settings.bungeecord", false);
}
private static void nettyThreads() {
int count = SpigotConfig.getInt("settings.netty-threads", 4);
System.setProperty("io.netty.eventLoopThreads", Integer.toString(count));
Bukkit.getLogger().log(Level.INFO, "Using {0} threads for Netty based IO", count);
}
public static boolean disableStatSaving;
public static Map<Identifier, Integer> forcedStats = new HashMap<>();
private static void stats() {
SpigotConfig.disableStatSaving = SpigotConfig.getBoolean("stats.disable-saving", false);
if (!SpigotConfig.config.contains("stats.forced-stats")) {
SpigotConfig.config.createSection("stats.forced-stats");
}
ConfigurationSection section = SpigotConfig.config.getConfigurationSection("stats.forced-stats");
for (String name : section.getKeys(true)) {
if (section.isInt(name)) {
try {
Identifier key = Identifier.parse(name);
if (BuiltInRegistries.CUSTOM_STAT.get(key) == null) {
Bukkit.getLogger().log(Level.WARNING, "Ignoring non existent stats.forced-stats " + name);
continue;
}
SpigotConfig.forcedStats.put(key, section.getInt(name));
} catch (Exception ex) {
Bukkit.getLogger().log(Level.WARNING, "Ignoring invalid stats.forced-stats " + name);
}
}
}
}
private static void tpsCommand() {
SpigotConfig.commands.put("tps", new TicksPerSecondCommand("tps"));
}
public static int playerSample;
private static void playerSample() {
SpigotConfig.playerSample = Math.max(SpigotConfig.getInt("settings.sample-count", 12), 0); // Paper - Avoid negative counts
Bukkit.getLogger().log(Level.INFO, "Server Ping Player Sample Count: {0}", playerSample); // Paper - Use logger
}
public static int playerShuffle;
private static void playerShuffle() {
SpigotConfig.playerShuffle = SpigotConfig.getInt("settings.player-shuffle", 0);
}
public static boolean enableSpamExclusions = false;
public static List<String> spamExclusions;
private static void spamExclusions() {
SpigotConfig.spamExclusions = SpigotConfig.getList("commands.spam-exclusions", List.of("/skill"));
Object enabled = SpigotConfig.config.get("commands.enable-spam-exclusions");
if (enabled instanceof Boolean value) {
SpigotConfig.enableSpamExclusions = value;
} else {
if (spamExclusions.size() == 1 && Objects.equals(spamExclusions.getFirst(), "/skill")) {
SpigotConfig.enableSpamExclusions = false;
SpigotConfig.set("commands.enable-spam-exclusions", false);
} else {
SpigotConfig.enableSpamExclusions = true;
SpigotConfig.set("commands.enable-spam-exclusions", true);
}
}
}
public static boolean silentCommandBlocks;
private static void silentCommandBlocks() {
SpigotConfig.silentCommandBlocks = SpigotConfig.getBoolean("commands.silent-commandblock-console", false);
}
public static Set<String> replaceCommands;
private static void replaceCommands() {
if (SpigotConfig.config.contains("replace-commands")) {
SpigotConfig.set("commands.replace-commands", SpigotConfig.config.getStringList("replace-commands"));
SpigotConfig.config.set("replace-commands", null);
}
SpigotConfig.replaceCommands = new HashSet<>(SpigotConfig.getList("commands.replace-commands",
Arrays.asList("setblock", "summon", "testforblock", "tellraw")));
}
public static int userCacheCap;
private static void userCacheCap() {
SpigotConfig.userCacheCap = SpigotConfig.getInt("settings.user-cache-size", 1000);
}
public static boolean saveUserCacheOnStopOnly;
private static void saveUserCacheOnStopOnly() {
SpigotConfig.saveUserCacheOnStopOnly = SpigotConfig.getBoolean("settings.save-user-cache-on-stop-only", false);
}
public static double movedWronglyThreshold;
private static void movedWronglyThreshold() {
SpigotConfig.movedWronglyThreshold = SpigotConfig.getDouble("settings.moved-wrongly-threshold", 0.0625);
}
public static double movedTooQuicklyMultiplier;
private static void movedTooQuicklyMultiplier() {
SpigotConfig.movedTooQuicklyMultiplier = SpigotConfig.getDouble("settings.moved-too-quickly-multiplier", 10.0);
}
public static double maxAbsorption = 2048;
public static double maxHealth = 1024;
public static double movementSpeed = 1024;
public static double attackDamage = 2048;
private static void attributeMaxes() {
SpigotConfig.maxAbsorption = SpigotConfig.getDouble("settings.attribute.maxAbsorption.max", SpigotConfig.maxAbsorption);
((RangedAttribute) Attributes.MAX_ABSORPTION.value()).maxValue = SpigotConfig.maxAbsorption;
SpigotConfig.maxHealth = SpigotConfig.getDouble("settings.attribute.maxHealth.max", SpigotConfig.maxHealth);
((RangedAttribute) Attributes.MAX_HEALTH.value()).maxValue = SpigotConfig.maxHealth;
SpigotConfig.movementSpeed = SpigotConfig.getDouble("settings.attribute.movementSpeed.max", SpigotConfig.movementSpeed);
((RangedAttribute) Attributes.MOVEMENT_SPEED.value()).maxValue = SpigotConfig.movementSpeed;
SpigotConfig.attackDamage = SpigotConfig.getDouble("settings.attribute.attackDamage.max", SpigotConfig.attackDamage);
((RangedAttribute) Attributes.ATTACK_DAMAGE.value()).maxValue = SpigotConfig.attackDamage;
}
public static boolean debug;
private static void debug() {
SpigotConfig.debug = SpigotConfig.getBoolean("settings.debug", false);
if (SpigotConfig.debug && !LogManager.getRootLogger().isTraceEnabled()) {
// Enable debug logging
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration conf = ctx.getConfiguration();
conf.getLoggerConfig(LogManager.ROOT_LOGGER_NAME).setLevel(org.apache.logging.log4j.Level.ALL);
ctx.updateLoggers(conf);
}
if (LogManager.getRootLogger().isTraceEnabled()) {
Bukkit.getLogger().info("Debug logging is enabled");
}
}
public static boolean disableAdvancementSaving;
public static List<String> disabledAdvancements;
private static void disabledAdvancements() {
SpigotConfig.disableAdvancementSaving = SpigotConfig.getBoolean("advancements.disable-saving", false);
SpigotConfig.disabledAdvancements = SpigotConfig.getList("advancements.disabled", List.of("minecraft:story/disabled"));
}
public static boolean logVillagerDeaths;
public static boolean logNamedDeaths;
private static void logDeaths() {
SpigotConfig.logVillagerDeaths = SpigotConfig.getBoolean("settings.log-villager-deaths", true);
SpigotConfig.logNamedDeaths = SpigotConfig.getBoolean("settings.log-named-deaths", true);
}
public static boolean disablePlayerDataSaving;
private static void disablePlayerDataSaving() {
SpigotConfig.disablePlayerDataSaving = SpigotConfig.getBoolean("players.disable-saving", false);
}
public static boolean belowZeroGenerationInExistingChunks;
private static void belowZeroGenerationInExistingChunks() {
SpigotConfig.belowZeroGenerationInExistingChunks = SpigotConfig.getBoolean("world-settings.default.below-zero-generation-in-existing-chunks", true);
}
}
@@ -1,472 +0,0 @@
package org.spigotmc;
import java.util.List;
import net.kyori.adventure.key.Key;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
public class SpigotWorldConfig {
private final String legacyWorldName;
private String worldName;
private final YamlConfiguration config;
private boolean verbose;
public SpigotWorldConfig(String legacyWorldName, Key worldKey) {
this.legacyWorldName = legacyWorldName;
this.worldName = worldKey.asString();
this.config = SpigotConfig.config;
this.init();
}
public void init() {
this.verbose = this.getBoolean("verbose", false); // Paper
if (SpigotConfig.version <= 12) {
ConfigurationSection section = this.config.getConfigurationSection("world-settings." + this.legacyWorldName);
if (section != null) {
this.config.set("world-settings." + this.legacyWorldName, null);
this.config.set("world-settings." + this.worldName, section);
Bukkit.getLogger().info("NOTE: Migrated spigot world config %s -> %s".formatted(this.legacyWorldName, this.worldName));
}
}
this.log("-------- World Settings For [" + this.worldName + "] --------");
SpigotConfig.readConfig(SpigotWorldConfig.class, this);
}
private void log(String s) {
if (this.verbose) {
Bukkit.getLogger().info(s);
}
}
private void set(String path, Object val) {
this.config.set("world-settings.default." + path, val);
}
public boolean getBoolean(String path, boolean def) {
this.config.addDefault("world-settings.default." + path, def);
return this.config.getBoolean("world-settings." + this.worldName + "." + path, this.config.getBoolean("world-settings.default." + path));
}
public double getDouble(String path, double def) {
this.config.addDefault("world-settings.default." + path, def);
return this.config.getDouble("world-settings." + this.worldName + "." + path, this.config.getDouble("world-settings.default." + path));
}
public int getInt(String path) {
return this.config.getInt("world-settings." + this.worldName + "." + path);
}
public int getInt(String path, int def) {
// Paper start - get int without setting default
return this.getInt(path, def, true);
}
public int getInt(String path, int def, boolean setDef) {
if (setDef) this.config.addDefault("world-settings.default." + path, def);
return this.config.getInt("world-settings." + this.worldName + "." + path, this.config.getInt("world-settings.default." + path, def));
// Paper end
}
public <T> List getList(String path, T def) {
this.config.addDefault("world-settings.default." + path, def);
return (List<T>) this.config.getList("world-settings." + this.worldName + "." + path, this.config.getList("world-settings.default." + path));
}
public String getString(String path, String def) {
this.config.addDefault("world-settings.default." + path, def);
return this.config.getString("world-settings." + this.worldName + "." + path, this.config.getString("world-settings.default." + path));
}
private Object get(String path, Object def) {
this.config.addDefault("world-settings.default." + path, def);
return this.config.get("world-settings." + this.worldName + "." + path, this.config.get("world-settings.default." + path));
}
// Crop growth rates
public int cactusModifier;
public int caneModifier;
public int melonModifier;
public int mushroomModifier;
public int pumpkinModifier;
public int saplingModifier;
public int beetrootModifier;
public int carrotModifier;
public int potatoModifier;
public int torchFlowerModifier;
public int wheatModifier;
public int wartModifier;
public int vineModifier;
public int cocoaModifier;
public int bambooModifier;
public int sweetBerryModifier;
public int kelpModifier;
public int twistingVinesModifier;
public int weepingVinesModifier;
public int caveVinesModifier;
public int glowBerryModifier;
public int pitcherPlantModifier;
private int getAndValidateGrowth(String crop) {
int modifier = this.getInt("growth." + crop.toLowerCase(java.util.Locale.ENGLISH) + "-modifier", 100);
if (modifier == 0) {
this.log("Cannot set " + crop + " growth to zero, defaulting to 100");
modifier = 100;
}
this.log(crop + " Growth Modifier: " + modifier + "%");
return modifier;
}
private void growthModifiers() {
this.cactusModifier = this.getAndValidateGrowth("Cactus");
this.caneModifier = this.getAndValidateGrowth("Cane");
this.melonModifier = this.getAndValidateGrowth("Melon");
this.mushroomModifier = this.getAndValidateGrowth("Mushroom");
this.pumpkinModifier = this.getAndValidateGrowth("Pumpkin");
this.saplingModifier = this.getAndValidateGrowth("Sapling");
this.beetrootModifier = this.getAndValidateGrowth("Beetroot");
this.carrotModifier = this.getAndValidateGrowth("Carrot");
this.potatoModifier = this.getAndValidateGrowth("Potato");
this.torchFlowerModifier = this.getAndValidateGrowth("TorchFlower");
this.wheatModifier = this.getAndValidateGrowth("Wheat");
this.wartModifier = this.getAndValidateGrowth("NetherWart");
this.vineModifier = this.getAndValidateGrowth("Vine");
this.cocoaModifier = this.getAndValidateGrowth("Cocoa");
this.bambooModifier = this.getAndValidateGrowth("Bamboo");
this.sweetBerryModifier = this.getAndValidateGrowth("SweetBerry");
this.kelpModifier = this.getAndValidateGrowth("Kelp");
this.twistingVinesModifier = this.getAndValidateGrowth("TwistingVines");
this.weepingVinesModifier = this.getAndValidateGrowth("WeepingVines");
this.caveVinesModifier = this.getAndValidateGrowth("CaveVines");
this.glowBerryModifier = this.getAndValidateGrowth("GlowBerry");
this.pitcherPlantModifier = this.getAndValidateGrowth("PitcherPlant");
}
public double itemMerge;
private void itemMerge() {
this.itemMerge = this.getDouble("merge-radius.item", 0.5);
this.log("Item Merge Radius: " + this.itemMerge);
}
public double expMerge;
private void expMerge() {
this.expMerge = this.getDouble("merge-radius.exp", -1);
this.log("Experience Merge Radius: " + this.expMerge);
}
public int viewDistance;
private void viewDistance() {
if (SpigotConfig.version < 12) {
this.set("view-distance", null);
}
Object viewDistanceObject = this.get("view-distance", "default");
this.viewDistance = (viewDistanceObject) instanceof Number ? ((Number) viewDistanceObject).intValue() : -1;
if (this.viewDistance <= 0) {
this.viewDistance = Bukkit.getViewDistance();
}
this.viewDistance = Math.max(Math.min(this.viewDistance, 32), 3);
this.log("View Distance: " + this.viewDistance);
}
public int simulationDistance;
private void simulationDistance() {
Object simulationDistanceObject = this.get("simulation-distance", "default");
this.simulationDistance = (simulationDistanceObject) instanceof Number ? ((Number) simulationDistanceObject).intValue() : -1;
if (this.simulationDistance <= 0) {
this.simulationDistance = Bukkit.getSimulationDistance();
}
this.log("Simulation Distance: " + this.simulationDistance);
}
public byte mobSpawnRange;
private void mobSpawnRange() {
this.mobSpawnRange = (byte) getInt("mob-spawn-range", 8); // Paper - Vanilla
this.log("Mob Spawn Range: " + this.mobSpawnRange);
}
public int itemDespawnRate;
private void itemDespawnRate() {
this.itemDespawnRate = this.getInt("item-despawn-rate", 6000);
this.log("Item Despawn Rate: " + this.itemDespawnRate);
}
public int animalActivationRange = 32;
public int monsterActivationRange = 32;
public int raiderActivationRange = 64;
public int miscActivationRange = 16;
public int flyingMonsterActivationRange = 32;
public int waterActivationRange = 16;
public int villagerActivationRange = 32;
public int wakeUpInactiveAnimals = 4;
public int wakeUpInactiveAnimalsEvery = 60 * 20;
public int wakeUpInactiveAnimalsFor = 5 * 20;
public int wakeUpInactiveMonsters = 8;
public int wakeUpInactiveMonstersEvery = 20 * 20;
public int wakeUpInactiveMonstersFor = 5 * 20;
public int wakeUpInactiveVillagers = 4;
public int wakeUpInactiveVillagersEvery = 30 * 20;
public int wakeUpInactiveVillagersFor = 5 * 20;
public int wakeUpInactiveFlying = 8;
public int wakeUpInactiveFlyingEvery = 10 * 20;
public int wakeUpInactiveFlyingFor = 5 * 20;
public int villagersWorkImmunityAfter = 5 * 20;
public int villagersWorkImmunityFor = 20;
public boolean villagersActiveForPanic = true;
public boolean tickInactiveVillagers = true;
public boolean ignoreSpectatorActivation = false;
private void activationRange() {
boolean hasAnimalsConfig = config.getInt("entity-activation-range.animals", this.animalActivationRange) != this.animalActivationRange; // Paper
this.animalActivationRange = this.getInt("entity-activation-range.animals", this.animalActivationRange);
this.monsterActivationRange = this.getInt("entity-activation-range.monsters", this.monsterActivationRange);
this.raiderActivationRange = this.getInt("entity-activation-range.raiders", this.raiderActivationRange);
this.miscActivationRange = this.getInt("entity-activation-range.misc", this.miscActivationRange);
this.waterActivationRange = this.getInt("entity-activation-range.water", this.waterActivationRange);
this.villagerActivationRange = this.getInt("entity-activation-range.villagers", hasAnimalsConfig ? this.animalActivationRange : this.villagerActivationRange);
this.flyingMonsterActivationRange = this.getInt("entity-activation-range.flying-monsters", this.flyingMonsterActivationRange);
this.wakeUpInactiveAnimals = this.getInt("entity-activation-range.wake-up-inactive.animals-max-per-tick", this.wakeUpInactiveAnimals);
this.wakeUpInactiveAnimalsEvery = this.getInt("entity-activation-range.wake-up-inactive.animals-every", this.wakeUpInactiveAnimalsEvery);
this.wakeUpInactiveAnimalsFor = this.getInt("entity-activation-range.wake-up-inactive.animals-for", this.wakeUpInactiveAnimalsFor);
this.wakeUpInactiveMonsters = this.getInt("entity-activation-range.wake-up-inactive.monsters-max-per-tick", this.wakeUpInactiveMonsters);
this.wakeUpInactiveMonstersEvery = this.getInt("entity-activation-range.wake-up-inactive.monsters-every", this.wakeUpInactiveMonstersEvery);
this.wakeUpInactiveMonstersFor = this.getInt("entity-activation-range.wake-up-inactive.monsters-for", this.wakeUpInactiveMonstersFor);
this.wakeUpInactiveVillagers = this.getInt("entity-activation-range.wake-up-inactive.villagers-max-per-tick", this.wakeUpInactiveVillagers);
this.wakeUpInactiveVillagersEvery = this.getInt("entity-activation-range.wake-up-inactive.villagers-every", this.wakeUpInactiveVillagersEvery);
this.wakeUpInactiveVillagersFor = this.getInt("entity-activation-range.wake-up-inactive.villagers-for", this.wakeUpInactiveVillagersFor);
this.wakeUpInactiveFlying = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-max-per-tick", this.wakeUpInactiveFlying);
this.wakeUpInactiveFlyingEvery = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-every", this.wakeUpInactiveFlyingEvery);
this.wakeUpInactiveFlyingFor = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-for", this.wakeUpInactiveFlyingFor);
this.villagersWorkImmunityAfter = this.getInt("entity-activation-range.villagers-work-immunity-after", this.villagersWorkImmunityAfter);
this.villagersWorkImmunityFor = this.getInt("entity-activation-range.villagers-work-immunity-for", this.villagersWorkImmunityFor);
this.villagersActiveForPanic = this.getBoolean("entity-activation-range.villagers-active-for-panic", this.villagersActiveForPanic);
this.tickInactiveVillagers = this.getBoolean("entity-activation-range.tick-inactive-villagers", this.tickInactiveVillagers);
this.ignoreSpectatorActivation = this.getBoolean("entity-activation-range.ignore-spectators", this.ignoreSpectatorActivation);
this.log("Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation);
}
public int playerTrackingRange = 128;
public int animalTrackingRange = 96;
public int monsterTrackingRange = 96;
public int miscTrackingRange = 96;
public int displayTrackingRange = 128;
public int otherTrackingRange = 64;
private void trackingRange() {
this.playerTrackingRange = this.getInt("entity-tracking-range.players", this.playerTrackingRange);
this.animalTrackingRange = this.getInt("entity-tracking-range.animals", this.animalTrackingRange);
this.monsterTrackingRange = this.getInt("entity-tracking-range.monsters", this.monsterTrackingRange);
this.miscTrackingRange = this.getInt("entity-tracking-range.misc", this.miscTrackingRange);
this.displayTrackingRange = this.getInt("entity-tracking-range.display", this.displayTrackingRange);
this.otherTrackingRange = this.getInt("entity-tracking-range.other", this.otherTrackingRange);
this.log("Entity Tracking Range: Pl " + this.playerTrackingRange + " / An " + this.animalTrackingRange + " / Mo " + this.monsterTrackingRange + " / Mi " + this.miscTrackingRange + " / Di " + this.displayTrackingRange + " / Other " + this.otherTrackingRange);
}
public int hopperTransfer;
public int hopperCheck;
public int hopperAmount;
public boolean hopperCanLoadChunks;
private void hoppers() {
// Set the tick delay between hopper item movements
this.hopperTransfer = this.getInt("ticks-per.hopper-transfer", 8);
if (SpigotConfig.version < 11) {
this.set("ticks-per.hopper-check", 1);
}
this.hopperCheck = this.getInt("ticks-per.hopper-check", 1);
this.hopperAmount = this.getInt("hopper-amount", 1);
this.hopperCanLoadChunks = this.getBoolean("hopper-can-load-chunks", false);
this.log("Hopper Transfer: " + this.hopperTransfer + " Hopper Check: " + this.hopperCheck + " Hopper Amount: " + this.hopperAmount + " Hopper Can Load Chunks: " + this.hopperCanLoadChunks);
}
public int arrowDespawnRate;
public int tridentDespawnRate;
private void arrowDespawnRate() {
this.arrowDespawnRate = this.getInt("arrow-despawn-rate", 1200);
this.tridentDespawnRate = this.getInt("trident-despawn-rate", this.arrowDespawnRate);
this.log("Arrow Despawn Rate: " + this.arrowDespawnRate + " Trident Despawn Rate:" + this.tridentDespawnRate);
}
public boolean zombieAggressiveTowardsVillager;
private void zombieAggressiveTowardsVillager() {
this.zombieAggressiveTowardsVillager = this.getBoolean("zombie-aggressive-towards-villager", true);
this.log("Zombie Aggressive Towards Villager: " + this.zombieAggressiveTowardsVillager);
}
public boolean nerfSpawnerMobs;
private void nerfSpawnerMobs() {
this.nerfSpawnerMobs = this.getBoolean("nerf-spawner-mobs", false);
this.log("Nerfing mobs spawned from spawners: " + this.nerfSpawnerMobs);
}
public boolean enableZombiePigmenPortalSpawns;
private void enableZombiePigmenPortalSpawns() {
this.enableZombiePigmenPortalSpawns = this.getBoolean("enable-zombie-pigmen-portal-spawns", true);
this.log("Allow Zombie Pigmen to spawn from portal blocks: " + this.enableZombiePigmenPortalSpawns);
}
public int dragonDeathSoundRadius;
private void keepDragonDeathPerWorld() {
this.dragonDeathSoundRadius = this.getInt("dragon-death-sound-radius", 0);
}
public int witherSpawnSoundRadius;
private void witherSpawnSoundRadius() {
this.witherSpawnSoundRadius = this.getInt("wither-spawn-sound-radius", 0);
}
public int endPortalSoundRadius;
private void endPortalSoundRadius() {
this.endPortalSoundRadius = this.getInt("end-portal-sound-radius", 0);
}
public int villageSeed;
public int desertSeed;
public int iglooSeed;
public int jungleSeed;
public int swampSeed;
public int monumentSeed;
public int oceanSeed;
public int outpostSeed;
public int shipwreckSeed;
public int endCitySeed;
public int netherSeed;
public int mansionSeed;
public int fossilSeed;
public int portalSeed;
public int ancientCitySeed;
public int trailRuinsSeed;
public int trialChambersSeed;
public int abandonedCampSeed;
public int buriedTreasureSeed;
public Integer mineshaftSeed;
public Long strongholdSeed;
public int slimeSeed;
private <N extends Number> N getSeed(String path, java.util.function.Function<String, N> toNumberFunc) {
final String value = this.getString(path, "default");
return org.apache.commons.lang3.math.NumberUtils.isParsable(value) ? toNumberFunc.apply(value) : null;
}
private void initWorldGenSeeds() {
this.villageSeed = this.getInt("seed-village", 10387312);
this.desertSeed = this.getInt("seed-desert", 14357617);
this.iglooSeed = this.getInt("seed-igloo", 14357618);
this.jungleSeed = this.getInt("seed-jungle", 14357619);
this.swampSeed = this.getInt("seed-swamp", 14357620);
this.monumentSeed = this.getInt("seed-monument", 10387313);
this.shipwreckSeed = this.getInt("seed-shipwreck", 165745295);
this.oceanSeed = this.getInt("seed-ocean", 14357621);
this.outpostSeed = this.getInt("seed-outpost", 165745296);
this.endCitySeed = this.getInt("seed-endcity", 10387313);
this.netherSeed = this.getInt("seed-nether", 30084232);
this.mansionSeed = this.getInt("seed-mansion", 10387319);
this.fossilSeed = this.getInt("seed-fossil", 14357921);
this.portalSeed = this.getInt("seed-portal", 34222645);
this.ancientCitySeed = this.getInt("seed-ancientcity", 20083232);
this.trailRuinsSeed = this.getInt("seed-trailruins", 83469867);
this.trialChambersSeed = this.getInt("seed-trialchambers", 94251327);
this.abandonedCampSeed = this.getInt("seed-abandonedcamp", 91231127);
this.buriedTreasureSeed = this.getInt("seed-buriedtreasure", 10387320); // AbstractSpreadingStructurePlacement#HIGHLY_ARBITRARY_RANDOM_SALT
this.mineshaftSeed = this.getSeed("seed-mineshaft", Integer::parseInt);
this.strongholdSeed = this.getSeed("seed-stronghold", Long::parseLong);
this.slimeSeed = this.getInt("seed-slime", 987234911);
this.log("Custom Map Seeds: " +
" Village: " + this.villageSeed +
" Desert: " + this.desertSeed +
" Igloo: " + this.iglooSeed +
" Jungle: " + this.jungleSeed +
" Swamp: " + this.swampSeed +
" Monument: " + this.monumentSeed +
" Shipwreck: " + this.shipwreckSeed +
" Ocean: " + this.oceanSeed +
" Outpost: " + this.outpostSeed +
" End City: " + this.endCitySeed +
" Nether: " + this.netherSeed +
" Mansion: " + this.mansionSeed +
" Fossil: " + this.fossilSeed +
" Portal: " + this.portalSeed +
" Ancient City: " + this.ancientCitySeed +
" Trail Ruins: " + this.trailRuinsSeed +
" Trial Chambers: " + this.trialChambersSeed +
" Abandoned Camp: " + this.abandonedCampSeed +
" Buried Treasure: " + this.buriedTreasureSeed +
" Mineshaft: " + this.mineshaftSeed +
" Stronghold: " + this.strongholdSeed +
" Slime: " + this.slimeSeed
);
}
public float jumpWalkExhaustion;
public float jumpSprintExhaustion;
public float combatExhaustion;
public float regenExhaustion;
public float swimMultiplier;
public float sprintMultiplier;
public float otherMultiplier;
private void initHunger() {
if (SpigotConfig.version < 10) {
this.set("hunger.walk-exhaustion", null);
this.set("hunger.sprint-exhaustion", null);
this.set("hunger.combat-exhaustion", 0.1);
this.set("hunger.regen-exhaustion", 6.0);
}
this.jumpWalkExhaustion = (float) this.getDouble("hunger.jump-walk-exhaustion", 0.05);
this.jumpSprintExhaustion = (float) this.getDouble("hunger.jump-sprint-exhaustion", 0.2);
this.combatExhaustion = (float) this.getDouble("hunger.combat-exhaustion", 0.1);
this.regenExhaustion = (float) this.getDouble("hunger.regen-exhaustion", 6.0);
this.swimMultiplier = (float) this.getDouble("hunger.swim-multiplier", 0.01);
this.sprintMultiplier = (float) this.getDouble("hunger.sprint-multiplier", 0.1);
this.otherMultiplier = (float) this.getDouble("hunger.other-multiplier", 0.0);
}
public int currentPrimedTnt = 0;
public int maxTntTicksPerTick;
private void maxTntPerTick() {
if (SpigotConfig.version < 7) {
this.set("max-tnt-per-tick", 100);
}
this.maxTntTicksPerTick = this.getInt("max-tnt-per-tick", 100);
this.log("Max TNT Explosions: " + this.maxTntTicksPerTick);
}
public int hangingTickFrequency;
private void hangingTickFrequency() {
this.hangingTickFrequency = this.getInt("hanging-tick-frequency", 100);
}
public int tileMaxTickTime;
public int entityMaxTickTime;
private void maxTickTimes() {
this.tileMaxTickTime = this.getInt("max-tick-time.tile", 50);
this.entityMaxTickTime = this.getInt("max-tick-time.entity", 50);
this.log("Tile Max Tick Time: " + this.tileMaxTickTime + "ms Entity max Tick Time: " + this.entityMaxTickTime + "ms");
}
public int thunderChance;
private void thunderChance() {
this.thunderChance = this.getInt("thunder-chance", 100000);
}
public boolean belowZeroGenerationInExistingChunks;
private void belowZeroGenerationInExistingChunks() {
this.belowZeroGenerationInExistingChunks = this.getBoolean("below-zero-generation-in-existing-chunks", true);
}
public boolean unloadFrozenChunks;
private void unloadFrozenChunks() {
this.unloadFrozenChunks = this.getBoolean("unload-frozen-chunks", false);
}
}
@@ -1,20 +0,0 @@
package org.spigotmc;
public class TickLimiter {
private final int maxTime;
private long startTime;
public TickLimiter(int maxTime) {
this.maxTime = maxTime;
}
public void initTick() {
this.startTime = System.currentTimeMillis();
}
public boolean shouldContinue() {
long remaining = System.currentTimeMillis() - this.startTime;
return remaining < this.maxTime;
}
}
@@ -25,9 +25,9 @@ public final class TrackingRange {
return defaultRange;
}
final SpigotWorldConfig config = entity.level().spigotConfig;
final io.papermc.paper.configuration.WorldConfiguration config = entity.level().paperConfig();
if (entity instanceof ServerPlayer) {
return config.playerTrackingRange;
return config.entities.trackingRange.player;
}
if (entity instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon) {
@@ -39,20 +39,20 @@ public final class TrackingRange {
case RAIDER:
case MONSTER:
case FLYING_MONSTER:
return config.monsterTrackingRange;
return config.entities.trackingRange.monster;
case WATER:
case VILLAGER:
case ANIMAL:
return config.animalTrackingRange;
return config.entities.trackingRange.animal;
case MISC:
}
if (entity instanceof ItemFrame || entity instanceof Painting || entity instanceof ItemEntity || entity instanceof ExperienceOrb) {
return config.miscTrackingRange;
return config.entities.trackingRange.misc;
} else if (entity instanceof Display) {
return config.displayTrackingRange;
return config.entities.trackingRange.display;
} else {
return config.otherTrackingRange;
return config.entities.trackingRange.other;
}
}
}
@@ -79,7 +79,7 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
logger.log(Level.SEVERE, "If you see a plugin in the Server thread dump below, then please report it to that author");
logger.log(Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring");
logger.log(Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once");
logger.log(Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes");
logger.log(Level.SEVERE, "\t If this is the case, consider increasing watchdog.timeout-seconds in paper-global.yml but note that this will replace the crash with LARGE lag spikes");
logger.log(Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues");
logger.log(Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports");
logger.log(Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion());
@@ -129,7 +129,7 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
if (!server.hasStopped()) {
server.forceTicks = true;
if (this.restart) {
RestartCommand.addShutdownHook(SpigotConfig.restartScript);
RestartCommand.addShutdownHook(io.papermc.paper.configuration.GlobalConfiguration.get().watchdog.restartScript);
}
// try one last chance to safe shutdown on main in case it 'comes back'
server.abnormalExit = true;
@@ -1,43 +0,0 @@
# This is the Bukkit configuration file in Paper.
# As you can see, there's actually not that much to configure without any plugins.
#
# If you need help with the configuration or have any questions related to Paper,
# join us in our Discord or check the docs page.
#
# File Reference: https://docs.papermc.io/paper/reference/bukkit-configuration/
# Docs: https://docs.papermc.io/
# Discord: https://discord.gg/papermc
# Website: https://papermc.io/
settings:
allow-end: true
warn-on-overload: true
permissions-file: permissions.yml
update-folder: update
plugin-profiling: false
connection-throttle: 4000
query-plugins: true
deprecated-verbose: default
shutdown-message: Server closed
minimum-api: none
use-map-color-cache: true
spawn-limits:
monsters: 70
animals: 10
water-animals: 5
water-ambient: 20
water-underground-creature: 5
axolotls: 5
ambient: 15
chunk-gc:
period-in-ticks: 600
ticks-per:
animal-spawns: 400
monster-spawns: 1
water-spawns: 1
water-ambient-spawns: 1
water-underground-creature-spawns: 1
axolotl-spawns: 1
ambient-spawns: 1
autosave: 6000
aliases: now-in-commands.yml
@@ -0,0 +1,145 @@
package io.papermc.paper.configuration;
import io.papermc.paper.configuration.mapping.LoadEarly;
import io.papermc.paper.configuration.yaml.YamlConfigurationLoader;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import joptsimple.OptionParser;
import org.bukkit.support.environment.Normal;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.spongepowered.configurate.ConfigurationNode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Makes sure the early global config load, which runs before {@code Bootstrap}, never touches anything that requires it.
* Test environments are always bootstrapped, so the load runs in a separate JVM that never bootstraps.
* Fails if a {@link LoadEarly} field, or any field initializer of a section containing one, requires the game to be bootstrapped.
*/
@Normal
public class EarlyGlobalConfigPreBootstrapTest {
private static final String DONE = "early global config loaded";
private static final String CLASS_INIT_LOG = "[class,init]";
// Initializing either of these before Bootstrap#bootStrap means registries were accessed
private static final List<String> BOOTSTRAP_CLASSES = List.of(
"net/minecraft/server/Bootstrap",
"net/minecraft/core/registries/BuiltInRegistries"
);
@TempDir
Path dir;
@Test
public void earlyLoadDoesNotRequireBootstrap() throws Exception {
final Path outputFile = this.dir.resolve("output.txt");
final Process process = new ProcessBuilder(
Path.of(System.getProperty("java.home"), "bin", "java").toString(),
"-Xlog:class+init=info:stdout",
"-cp", System.getProperty("java.class.path"),
EarlyLoad.class.getName(),
this.dir.toString()
)
.directory(this.dir.toFile())
.redirectErrorStream(true)
.redirectOutput(outputFile.toFile())
.start();
try {
assertTrue(process.waitFor(2, TimeUnit.MINUTES), "early load did not finish");
} finally {
process.destroyForcibly();
}
final List<String> output = Files.readAllLines(outputFile);
final String programOutput = output.stream().filter(line -> !line.contains(CLASS_INIT_LOG)).collect(Collectors.joining("\n"));
assertEquals(0, process.exitValue(), () -> "early load failed:\n" + programOutput);
assertTrue(output.contains(DONE), () -> "early load did not complete:\n" + programOutput);
String lastConfigClass = "<none>";
for (final String line : output) {
final String initialized = initializedClass(line);
if (initialized == null) {
continue;
}
if (initialized.startsWith("io/papermc/paper/configuration/")) {
lastConfigClass = initialized;
} else if (BOOTSTRAP_CLASSES.contains(initialized)) {
fail("The early global config load initialized " + initialized + ", which requires the game to be bootstrapped. "
+ "Check the @LoadEarly fields and the field initializers of the sections containing them. "
+ "Last initialized config class: " + lastConfigClass);
}
}
}
private static @Nullable String initializedClass(final String line) {
if (!line.contains(CLASS_INIT_LOG)) {
return null;
}
final int start = line.indexOf("Initializing '");
if (start == -1) {
return null;
}
final int nameStart = start + "Initializing '".length();
return line.substring(nameStart, line.indexOf('\'', nameStart));
}
// Runs in the separate JVM
public static final class EarlyLoad {
public static void main(final String[] args) throws Exception {
final Path dir = Path.of(args[0]);
// the defaults create every section containing early fields
final GlobalConfiguration defaults = PaperConfigurationsInitializer.loadEarlyGlobalConfig(dir.resolve("missing.yml"));
// write all early fields out, so loading them back goes through every early field's serializer
final Path configDir = dir.resolve("config");
Files.createDirectories(configDir);
final YamlConfigurationLoader loader = ConfigurationLoaders.naturallySorted()
.defaultOptions(options -> PaperConfigurations.earlyGlobalOptions(options, defaults))
.path(configDir.resolve(PaperConfigurations.GLOBAL_CONFIG_FILE_NAME))
.build();
final ConfigurationNode node = loader.createNode();
node.set(GlobalConfiguration.class, defaults);
loader.save(node);
// old versions go through every versioned transformation that doesn't require the server
final Path oldConfig = dir.resolve("old.yml");
Files.writeString(oldConfig, """
%s: 28
packet-limiter:
overrides:
ServerboundChatPacket:
max-packet-rate: 1.0
""".formatted(Configuration.VERSION_FIELD));
PaperConfigurationsInitializer.loadEarlyGlobalConfig(oldConfig);
// legacy files that weren't converted yet are read too
final Path bukkitConfig = dir.resolve("bukkit.yml");
final Path spigotConfig = dir.resolve("spigot.yml");
Files.writeString(bukkitConfig, "settings:\n world-container: worlds\n update-folder: update\n");
Files.writeString(spigotConfig, "advancements:\n disabled:\n - minecraft:story/root\n");
final OptionParser parser = new OptionParser();
parser.accepts("paper-settings-directory").withRequiredArg().ofType(File.class);
parser.accepts("bukkit-settings").withRequiredArg().ofType(File.class);
parser.accepts("spigot-settings").withRequiredArg().ofType(File.class);
PaperConfigurationsInitializer.load(parser.parse(
"--paper-settings-directory=" + configDir,
"--bukkit-settings=" + bukkitConfig,
"--spigot-settings=" + spigotConfig
));
PaperConfigurationsInitializer.earlyGlobalConfig();
System.out.println(DONE);
}
}
}
@@ -0,0 +1,70 @@
package io.papermc.paper.configuration;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@Normal
public class EarlyGlobalConfigTest {
@TempDir
Path dir;
@Test
public void onlyEarlyFieldsAreLoaded() throws Exception {
final Path file = this.dir.resolve(PaperConfigurations.GLOBAL_CONFIG_FILE_NAME);
Files.writeString(file, """
%s: %d
misc:
world-container: worlds
netty-threads: 13
advancements:
disabled:
- minecraft:story/root
proxies:
velocity:
enabled: true
""".formatted(Configuration.VERSION_FIELD, GlobalConfiguration.CURRENT_VERSION));
final GlobalConfiguration config = PaperConfigurationsInitializer.loadEarlyGlobalConfig(file);
assertEquals("worlds", config.misc.worldContainer);
assertEquals("update", config.misc.updateFolder);
assertEquals(Set.of("minecraft:story/root"), config.advancements.disabled);
assertEquals(4, config.misc.nettyThreads, "non-early field next to early ones must keep its default");
assertNotEquals("13", System.getProperty("io.netty.eventLoopThreads"), "post-processors must not run");
assertNull(config.proxies, "sections without early fields must not be created");
assertNull(config.chunkSystem, "sections without early fields must not be created");
}
@Test
public void oldVersionLoadsWithoutServer() throws Exception {
final Path file = this.dir.resolve(PaperConfigurations.GLOBAL_CONFIG_FILE_NAME);
Files.writeString(file, """
%s: 28
logging:
log-player-ip-addresses: false
misc:
world-container: worlds
""".formatted(Configuration.VERSION_FIELD));
final GlobalConfiguration config = PaperConfigurationsInitializer.loadEarlyGlobalConfig(file);
assertEquals("worlds", config.misc.worldContainer);
}
@Test
public void defaultsWithoutFile() throws Exception {
final GlobalConfiguration config = PaperConfigurationsInitializer.loadEarlyGlobalConfig(this.dir.resolve("missing.yml"));
assertEquals(".", config.misc.worldContainer);
assertEquals("update", config.misc.updateFolder);
assertEquals(Set.of("minecraft:story/disabled"), config.advancements.disabled);
assertNull(config.proxies);
}
}
@@ -1,7 +1,6 @@
package io.papermc.paper.configuration;
import net.minecraft.core.RegistryAccess;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
public final class GlobalConfigTestingBase {
@@ -9,10 +8,8 @@ public final class GlobalConfigTestingBase {
public static void setupGlobalConfigForTest(RegistryAccess registryAccess) {
//noinspection ConstantConditions
if (GlobalConfiguration.get() == null) {
ConfigurationNode node = PaperConfigurations.createForTesting(registryAccess);
try {
GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class);
GlobalConfiguration.set(globalConfiguration);
GlobalConfiguration.set(PaperConfigurations.createGlobalForTesting(registryAccess));
} catch (SerializationException e) {
throw new RuntimeException(e);
}
@@ -0,0 +1,85 @@
package io.papermc.paper.configuration;
import io.papermc.paper.configuration.Configurations.ContextMap;
import io.papermc.paper.configuration.transformation.ServerPropertiesAccess;
import java.util.Properties;
import java.util.function.Consumer;
import net.minecraft.server.dedicated.DedicatedServerProperties;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.Test;
import org.spongepowered.configurate.BasicConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Normal
public class GlobalTransformationsTest {
private static final class FakeServerProperties implements ServerPropertiesAccess {
private DedicatedServerProperties current;
private FakeServerProperties(final Properties properties) {
this.current = new DedicatedServerProperties(properties, null);
}
@Override
public DedicatedServerProperties current() {
return this.current;
}
@Override
public void update(final Consumer<Properties> editor) {
final Properties copy = new Properties();
copy.putAll(this.current.properties);
editor.accept(copy);
this.current = new DedicatedServerProperties(copy, null);
}
}
private static ConfigurationNode node(final int version) throws SerializationException {
final ConfigurationNode node = BasicConfigurationNode.root();
node.node(Configuration.VERSION_FIELD).set(version);
return node;
}
@Test
public void missingContextFailsFullLoad() throws SerializationException {
final ConfigurationNode node = node(30);
final IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> PaperConfigurations.applyGlobalTransformations(ContextMap.builder().build(), node));
assertTrue(exception.getMessage().contains("server properties"), exception.getMessage());
}
@Test
public void earlyLoadSkipsTransformationsMissingContext() throws Exception {
final ConfigurationNode node = node(30);
node.node("player-auto-save", "rate").set(5);
PaperConfigurations.applyEarlyGlobalTransformations(node);
assertEquals(5, node.node("players", "auto-save", "rate").getInt(), "transformations without requirements still run early");
assertTrue(node.node("misc", "enable-nether").virtual(), "V31 requires server properties and must be skipped");
}
@Test
public void serverPropertiesAreMigrated() throws Exception {
final Properties properties = new Properties();
properties.setProperty("allow-nether", "false");
properties.setProperty("log-ips", "true");
final FakeServerProperties serverProperties = new FakeServerProperties(properties);
final ContextMap context = ContextMap.builder().put(Configurations.SERVER_PROPERTIES, serverProperties).build();
final ConfigurationNode node = node(28);
node.node("logging", "log-player-ip-addresses").set(false);
PaperConfigurations.applyGlobalTransformations(context, node);
assertFalse(serverProperties.current().logIPs, "V29 moves the option to server.properties");
assertTrue(node.node("logging", "log-player-ip-addresses").virtual());
assertFalse(node.node("misc", "enable-nether").getBoolean(true), "V31 moves the option from server.properties");
assertFalse(serverProperties.current().properties.containsKey("allow-nether"));
assertEquals(GlobalConfiguration.CURRENT_VERSION, node.node(Configuration.VERSION_FIELD).getInt());
}
}
@@ -0,0 +1,92 @@
package io.papermc.paper.configuration.constraint;
import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import io.papermc.paper.configuration.type.number.LongOr;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.Test;
import org.spongepowered.configurate.BasicConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.serialize.SerializationException;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
@Normal
public class ConstraintsTest {
@ConfigSerializable
static class Ranged {
@Constraints.Min(1)
@Constraints.Max(10)
int primitiveInt = 5;
@Constraints.Min(0.5)
double primitiveDouble = 1;
@Constraints.Min(1)
IntOr.Default intOr = IntOr.Default.USE_DEFAULT;
@Constraints.Max(10)
LongOr.Default longOr = LongOr.Default.USE_DEFAULT;
@Constraints.Min(0.5)
DoubleOr.Default doubleOr = DoubleOr.Default.USE_DEFAULT;
}
@ConfigSerializable
static class Unsupported {
@Constraints.Min(1)
String value = "a";
}
private static ConfigurationNode node() {
final ObjectMapper.Factory factory = ObjectMapper.factoryBuilder()
.addConstraint(Constraints.Min.class, new Constraints.Min.Factory())
.addConstraint(Constraints.Max.class, new Constraints.Max.Factory())
.build();
return BasicConfigurationNode.root(ConfigurationOptions.defaults().serializers(builder -> builder
.registerAnnotatedObjects(factory)
.register(IntOr.Default.SERIALIZER)
.register(LongOr.Default.SERIALIZER)
.register(DoubleOr.Default.SERIALIZER)));
}
private static void assertRejected(final String key, final Object value) throws SerializationException {
final ConfigurationNode node = node();
node.node(key).set(value);
assertThrows(SerializationException.class, () -> node.get(Ranged.class), key + "=" + value + " should be rejected");
}
@Test
public void inRangeValuesAreAccepted() throws SerializationException {
final ConfigurationNode node = node();
node.node("primitive-int").set(10);
node.node("primitive-double").set(0.5);
node.node("int-or").set(1);
node.node("long-or").set(10L);
node.node("double-or").set(0.5);
assertDoesNotThrow(() -> node.get(Ranged.class));
}
@Test
public void defaultValuesAreNotRangeChecked() {
final ConfigurationNode node = node();
assertDoesNotThrow(() -> node.get(Ranged.class));
}
@Test
public void outOfRangeValuesAreRejected() throws SerializationException {
assertRejected("primitive-int", 0);
assertRejected("primitive-int", 11);
assertRejected("primitive-double", 0.4);
assertRejected("int-or", 0);
assertRejected("long-or", 11L);
assertRejected("double-or", 0.4);
}
@Test
public void unsupportedTypesFailWhenMapperIsCreated() {
final ConfigurationNode node = node();
assertThrows(Exception.class, () -> node.get(Unsupported.class));
}
}
@@ -0,0 +1,59 @@
package io.papermc.paper.configuration.mapping;
import io.papermc.paper.configuration.ConfigurationPart;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.Test;
import org.spongepowered.configurate.BasicConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.serialize.SerializationException;
import static io.leangen.geantyref.GenericTypeReflector.erase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@Normal
public class InnerClassInstanceFactoryTest {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Rejected {
}
private static final Definition<Rejected, String, FieldProcessor.Factory<Rejected, String>> REJECTED = new Definition<>(
Rejected.class,
String.class,
(data, annotatedType) -> (target, deserializedValue, valueInField) -> {
throw new SerializationException(target.getType(), "rejected " + deserializedValue);
}
);
public static class Part extends ConfigurationPart {
@Rejected
public String first = "a";
@Rejected
public String second = "b";
public String plain = "c";
}
@Test
public void processorFailuresAreThrown() throws Exception {
final List<Definition<?, ?, ? extends FieldProcessor.Factory<?, ?>>> processors = List.of(REJECTED);
final ObjectMapper.Factory factory = ObjectMapper.factoryBuilder()
.addDiscoverer(InnerClassFieldDiscoverer.globalConfig(processors))
.build();
final ConfigurationNode node = BasicConfigurationNode.root(ConfigurationOptions.defaults()
.serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer())));
node.node("first").raw("x");
node.node("second").raw("y");
final SerializationException exception = assertThrows(SerializationException.class, () -> node.get(Part.class));
assertEquals(1, exception.getSuppressed().length, "every failing field must be reported");
}
}
@@ -1,7 +1,7 @@
package io.papermc.paper.world.structure;
import io.papermc.paper.configuration.PaperConfigurations;
import java.io.File;
import io.papermc.paper.configuration.WorldConfiguration;
import java.lang.reflect.Field;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
@@ -11,13 +11,9 @@ import net.minecraft.world.level.levelgen.structure.BuiltinStructureSets;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.support.RegistryHelper;
import org.bukkit.support.environment.AllFeatures;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.spigotmc.SpigotConfig;
import org.spigotmc.SpigotWorldConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,14 +22,7 @@ public class StructureSeedConfigTest {
@Test
public void checkStructureSeedDefaults() throws ReflectiveOperationException {
SpigotConfig.config = new YamlConfiguration() {
@Override
public void save(final @NotNull File file) {
// no-op
}
};
final SpigotWorldConfig config = PaperConfigurations.SPIGOT_WORLD_DEFAULTS.get();
final WorldConfiguration.Seeds seeds = PaperConfigurations.createWorldDefaultsForTesting().new Seeds();
final Registry<StructureSet> structureSets = RegistryHelper.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET);
for (final ResourceKey<StructureSet> setKey : structureSets.registryKeySet()) {
@@ -48,27 +37,27 @@ public class StructureSeedConfigTest {
continue;
}
int salt = switch (setKey.identifier().getPath()) {
case "villages" -> config.villageSeed;
case "desert_pyramids" -> config.desertSeed;
case "igloos" -> config.iglooSeed;
case "jungle_temples" -> config.jungleSeed;
case "swamp_huts" -> config.swampSeed;
case "pillager_outposts" -> config.outpostSeed;
case "ocean_monuments" -> config.monumentSeed;
case "woodland_mansions" -> config.mansionSeed;
case "buried_treasures" -> config.buriedTreasureSeed;
case "mineshafts" -> config.mineshaftSeed == null ? 0 : config.mineshaftSeed; // mineshaft seed is set differently
case "ruined_portals" -> config.portalSeed;
case "shipwrecks" -> config.shipwreckSeed;
case "ocean_ruins" -> config.oceanSeed;
case "nether_complexes" -> config.netherSeed;
case "nether_fossils" -> config.fossilSeed;
case "end_cities" -> config.endCitySeed;
case "ancient_cities" -> config.ancientCitySeed;
case "trail_ruins" -> config.trailRuinsSeed;
case "trial_chambers" -> config.trialChambersSeed;
case "abandoned_camp" -> config.abandonedCampSeed;
default -> throw new AssertionError("Missing structure set seed in SpigotWorldConfig for " + setKey);
case "villages" -> seeds.village;
case "desert_pyramids" -> seeds.desert;
case "igloos" -> seeds.igloo;
case "jungle_temples" -> seeds.jungle;
case "swamp_huts" -> seeds.swamp;
case "pillager_outposts" -> seeds.outpost;
case "ocean_monuments" -> seeds.monument;
case "woodland_mansions" -> seeds.mansion;
case "buried_treasures" -> seeds.buriedTreasure;
case "mineshafts" -> seeds.mineshaft.or(0); // mineshaft seed is set differently
case "ruined_portals" -> seeds.portal;
case "shipwrecks" -> seeds.shipwreck;
case "ocean_ruins" -> seeds.ocean;
case "nether_complexes" -> seeds.nether;
case "nether_fossils" -> seeds.fossil;
case "end_cities" -> seeds.endCity;
case "ancient_cities" -> seeds.ancientCity;
case "trail_ruins" -> seeds.trailRuins;
case "trial_chambers" -> seeds.trialChambers;
case "abandoned_camp" -> seeds.abandonedCamp;
default -> throw new AssertionError("Missing structure set seed in WorldConfiguration.Seeds for " + setKey);
};
if (setKey == BuiltinStructureSets.BURIED_TREASURES) {
final Field field = AbstractSpreadingStructurePlacement.class.getDeclaredField("HIGHLY_ARBITRARY_RANDOM_SALT");
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.inventory;
import io.papermc.paper.configuration.yaml.YamlConfigurationLoader;
import io.papermc.paper.datacomponent.DataComponentTypes;
import io.papermc.paper.datacomponent.item.ItemEnchantments;
import io.papermc.paper.datacomponent.item.Tool;
@@ -22,7 +23,6 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import static org.junit.jupiter.api.Assertions.assertEquals;