diff --git a/patches/server/Configurable-chance-of-villager-zombie-infection.patch b/patches/server/Configurable-chance-of-villager-zombie-infection.patch index f034688013..6afe332553 100644 --- a/patches/server/Configurable-chance-of-villager-zombie-infection.patch +++ b/patches/server/Configurable-chance-of-villager-zombie-infection.patch @@ -19,16 +19,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 - Villager entityvillager = (Villager) other; - - if (world.getDifficulty() != Difficulty.HARD && this.random.nextBoolean()) { -+ // Paper start -+ if (this.level().paperConfig().entities.behavior.zombieVillagerInfectionChance != 0.0 && (this.level().paperConfig().entities.behavior.zombieVillagerInfectionChance != -1.0 || world.getDifficulty() == Difficulty.NORMAL || world.getDifficulty() == Difficulty.HARD) && other instanceof Villager) { -+ if (this.level().paperConfig().entities.behavior.zombieVillagerInfectionChance == -1.0 && world.getDifficulty() != Difficulty.HARD && this.random.nextBoolean()) { - return flag; - } -+ if (this.level().paperConfig().entities.behavior.zombieVillagerInfectionChance != -1.0 && (this.random.nextDouble() * 100.0) > this.level().paperConfig().entities.behavior.zombieVillagerInfectionChance) { -+ return flag; -+ } // Paper end -+ -+ Villager entityvillager = (Villager) other; +- return flag; +- } ++ final double fallbackChance = world.getDifficulty() == Difficulty.HARD ? 1d : world.getDifficulty() == Difficulty.NORMAL ? 0.5d : 0d; // Paper ++ if (this.random.nextDouble() < world.paperConfig().entities.behavior.zombieVillagerInfectionChance.or(fallbackChance) && other instanceof Villager entityvillager) { // Paper // CraftBukkit start flag = Zombie.zombifyVillager(world, entityvillager, this.blockPosition(), this.isSilent(), CreatureSpawnEvent.SpawnReason.INFECTION) == null; } diff --git a/patches/server/Configurable-chat-thread-limit.patch b/patches/server/Configurable-chat-thread-limit.patch index b3752fce97..14d9f85bb3 100644 --- a/patches/server/Configurable-chat-thread-limit.patch +++ b/patches/server/Configurable-chat-thread-limit.patch @@ -26,22 +26,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -0,0 +0,0 @@ public class GlobalConfiguration extends ConfigurationPart { - public Misc misc; - public class Misc extends ConfigurationPart { -+ -+ public ChatThreads chatThreads; - public class ChatThreads extends ConfigurationPart.Post { - private int chatExecutorCoreSize = -1; - private int chatExecutorMaxSize = -1; - - @Override - public void postProcess() { -- // TODO: FILL + @PostProcess + private void postProcess() { +- // TODO: fill in separate patch + //noinspection ConstantConditions + if (net.minecraft.server.MinecraftServer.getServer() == null) return; // In testing env, this will be null here -+ int _chatExecutorMaxSize = (chatExecutorMaxSize <= 0) ? Integer.MAX_VALUE : chatExecutorMaxSize; // This is somewhat dumb, but, this is the default, do we cap this?; -+ int _chatExecutorCoreSize = Math.max(chatExecutorCoreSize, 0); ++ int _chatExecutorMaxSize = (this.chatExecutorMaxSize <= 0) ? Integer.MAX_VALUE : this.chatExecutorMaxSize; // This is somewhat dumb, but, this is the default, do we cap this?; ++ int _chatExecutorCoreSize = Math.max(this.chatExecutorCoreSize, 0); + + if (_chatExecutorMaxSize < _chatExecutorCoreSize) { + _chatExecutorMaxSize = _chatExecutorCoreSize; diff --git a/patches/server/Configurable-max-block-light-for-monster-spawning.patch b/patches/server/Configurable-max-block-light-for-monster-spawning.patch index a0f666964d..834d7d7024 100644 --- a/patches/server/Configurable-max-block-light-for-monster-spawning.patch +++ b/patches/server/Configurable-max-block-light-for-monster-spawning.patch @@ -13,7 +13,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 } else { DimensionType dimensionType = world.dimensionType(); - int i = dimensionType.monsterSpawnBlockLightLimit(); -+ int i = world.getLevel().paperConfig().entities.spawning.monsterSpawnMaxLightLevel >= 0 ? world.getLevel().paperConfig().entities.spawning.monsterSpawnMaxLightLevel : dimensionType.monsterSpawnBlockLightLimit(); // Paper ++ int i = world.getLevel().paperConfig().entities.spawning.monsterSpawnMaxLightLevel.or(dimensionType.monsterSpawnBlockLightLimit()); // Paper if (i < 15 && world.getBrightness(LightLayer.BLOCK, pos) > i) { return false; } else { diff --git a/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch b/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch index 3a865fb847..5002195eef 100644 --- a/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch +++ b/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch @@ -11,7 +11,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +++ b/build.gradle.kts @@ -0,0 +0,0 @@ dependencies { testImplementation("org.mockito:mockito-core:4.9.0") // Paper - switch to mockito - implementation("org.spongepowered:configurate-yaml:4.1.2") // Paper - config files + implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files implementation("commons-lang:commons-lang:2.6") + implementation("net.fabricmc:mapping-io:0.3.0") // Paper - needed to read mappings for stacktrace deobfuscation runtimeOnly("org.xerial:sqlite-jdbc:3.42.0.1") diff --git a/patches/server/Paper-Plugins.patch b/patches/server/Paper-Plugins.patch index 9a8891abd2..16f9035875 100644 --- a/patches/server/Paper-Plugins.patch +++ b/patches/server/Paper-Plugins.patch @@ -5192,7 +5192,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + try { + result.add(Permission.loadPermission(entry.getKey().toString(), (Map) entry.getValue(), permissionDefault, result)); + } catch (Throwable ex) { -+ throw new SerializationException(null, "Error loading permission %s".formatted(entry.getKey()), ex); ++ throw new SerializationException((Type) null, "Error loading permission %s".formatted(entry.getKey()), ex); + } + } + } diff --git a/patches/server/Paper-config-files.patch b/patches/server/Paper-config-files.patch index 3503c65f3b..57b2c42a6f 100644 --- a/patches/server/Paper-config-files.patch +++ b/patches/server/Paper-config-files.patch @@ -22,7 +22,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 implementation("org.ow2.asm:asm:9.5") implementation("org.ow2.asm:asm-commons:9.5") // Paper - ASM event executor generation testImplementation("org.mockito:mockito-core:4.9.0") // Paper - switch to mockito -+ implementation("org.spongepowered:configurate-yaml:4.1.2") // Paper - config files ++ implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files implementation("commons-lang:commons-lang:2.6") runtimeOnly("org.xerial:sqlite-jdbc:3.42.0.1") runtimeOnly("com.mysql:mysql-connector-j:8.1.0") @@ -114,13 +114,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @@ -0,0 +0,0 @@ +package io.papermc.paper.configuration; + -+abstract class ConfigurationPart { -+ -+ public static abstract class Post extends ConfigurationPart { -+ -+ public abstract void postProcess(); -+ } -+ ++/** ++ * Marker interface for unique sections of a configuration. ++ */ ++public abstract class ConfigurationPart { +} diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java new file mode 100644 @@ -448,16 +445,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +package io.papermc.paper.configuration; + +import co.aikar.timings.MinecraftTimings; -+import io.papermc.paper.configuration.constraint.Constraint; ++import com.mojang.logging.LogUtils; +import io.papermc.paper.configuration.constraint.Constraints; -+import io.papermc.paper.configuration.type.IntOr; ++import io.papermc.paper.configuration.type.number.IntOr; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; +import org.checkerframework.checker.nullness.qual.Nullable; ++import org.slf4j.Logger; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; ++import org.spongepowered.configurate.objectmapping.meta.PostProcess; +import org.spongepowered.configurate.objectmapping.meta.Required; +import org.spongepowered.configurate.objectmapping.meta.Setting; + @@ -467,7 +466,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + +@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) +public class GlobalConfiguration extends ConfigurationPart { -+ static final int CURRENT_VERSION = 29; ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ static final int CURRENT_VERSION = 29; // (when you change the version, change the comment, so it conflicts on rebases): + private static GlobalConfiguration instance; + public static GlobalConfiguration get() { + return instance; @@ -495,9 +495,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public boolean useDisplayNameInQuitMessage = false; + } + ++ @Deprecated(forRemoval = true) + public Timings timings; + -+ public class Timings extends ConfigurationPart.Post { ++ @Deprecated(forRemoval = true) ++ public class Timings extends ConfigurationPart { + public boolean enabled = true; + public boolean verbose = true; + public String url = "https://timings.aikar.co/"; @@ -510,8 +512,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public int historyLength = 3600; + public String serverName = "Unknown Server"; + -+ @Override -+ public void postProcess() { ++ @PostProcess ++ private void postProcess() { + MinecraftTimings.processConfig(this); + } + } @@ -525,13 +527,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public boolean onlineMode = true; + } + -+ @Constraint(Constraints.Velocity.class) + public Velocity velocity; + + public class Velocity extends ConfigurationPart { + public boolean enabled = false; + public boolean onlineMode = false; + public String secret = ""; ++ ++ @PostProcess ++ private void postProcess() { ++ if (this.enabled && this.secret.isEmpty()) { ++ LOGGER.error("Velocity is enabled, but no secret key was specified. A secret key is required. Disabling velocity..."); ++ this.enabled = false; ++ } ++ } + } + public boolean proxyProtocol = false; + public boolean isProxyOnlineMode() { @@ -622,16 +631,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public boolean saveEmptyScoreboardTeams = false; + } + ++ @SuppressWarnings("unused") // used in postProcess + public ChunkSystem chunkSystem; + -+ public class ChunkSystem extends ConfigurationPart.Post { ++ public class ChunkSystem extends ConfigurationPart { + + public int ioThreads = -1; + public int workerThreads = -1; + public String genParallelism = "default"; + -+ @Override -+ public void postProcess() { ++ @PostProcess ++ private void postProcess() { + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); + } + } @@ -708,13 +718,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public Misc misc; + + public class Misc extends ConfigurationPart { -+ public class ChatThreads extends ConfigurationPart.Post { ++ ++ @SuppressWarnings("unused") // used in postProcess ++ public ChatThreads chatThreads; ++ public class ChatThreads extends ConfigurationPart { + private int chatExecutorCoreSize = -1; + private int chatExecutorMaxSize = -1; + -+ @Override -+ public void postProcess() { -+ // TODO: FILL ++ @PostProcess ++ private void postProcess() { ++ // TODO: fill in separate patch + } + } + public int maxJoinsPerTick = 5; @@ -738,179 +751,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public boolean disableMushroomBlockUpdates = false; + } +} -diff --git a/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/configuration/InnerClassFieldDiscoverer.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.configuration; -+ -+import io.leangen.geantyref.GenericTypeReflector; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.spongepowered.configurate.objectmapping.FieldDiscoverer; -+import org.spongepowered.configurate.serialize.SerializationException; -+import org.spongepowered.configurate.util.CheckedSupplier; -+ -+import java.lang.reflect.AnnotatedType; -+import java.lang.reflect.Constructor; -+import java.lang.reflect.Field; -+import java.lang.reflect.Modifier; -+import java.util.Collections; -+import java.util.HashMap; -+import java.util.Iterator; -+import java.util.Map; -+ -+import static io.leangen.geantyref.GenericTypeReflector.erase; -+ -+final class InnerClassFieldDiscoverer implements FieldDiscoverer> { -+ -+ private final Map, Object> instanceMap = new HashMap<>(); -+ private final Map, Object> overrides; -+ @SuppressWarnings("unchecked") -+ private final FieldDiscoverer> delegate = (FieldDiscoverer>) FieldDiscoverer.object(target -> { -+ final Class type = erase(target.getType()); -+ if (this.overrides().containsKey(type)) { -+ this.instanceMap.put(type, this.overrides().get(type)); -+ return () -> this.overrides().get(type); -+ } -+ if (ConfigurationPart.class.isAssignableFrom(type) && !this.instanceMap.containsKey(type)) { -+ try { -+ final Constructor constructor; -+ final CheckedSupplier instanceSupplier; -+ if (type.getEnclosingClass() != null && !Modifier.isStatic(type.getModifiers())) { -+ final @Nullable Object instance = this.instanceMap.get(type.getEnclosingClass()); -+ if (instance == null) { -+ throw new SerializationException("Cannot create a new instance of an inner class " + type.getName() + " without an instance of its enclosing class " + type.getEnclosingClass().getName()); -+ } -+ constructor = type.getDeclaredConstructor(type.getEnclosingClass()); -+ instanceSupplier = () -> constructor.newInstance(instance); -+ } else { -+ constructor = type.getDeclaredConstructor(); -+ instanceSupplier = constructor::newInstance; -+ } -+ constructor.setAccessible(true); -+ final Object instance = instanceSupplier.get(); -+ this.instanceMap.put(type, instance); -+ return () -> instance; -+ } catch (ReflectiveOperationException e) { -+ throw new SerializationException(ConfigurationPart.class, target + " must be a valid ConfigurationPart", e); -+ } -+ } else { -+ throw new SerializationException(target + " must be a valid ConfigurationPart"); -+ } -+ }, "Object must be a unique ConfigurationPart"); -+ -+ InnerClassFieldDiscoverer(Map, Object> overrides) { -+ this.overrides = overrides; -+ } -+ -+ @Override -+ public @Nullable InstanceFactory> discover(AnnotatedType target, FieldCollector, V> collector) throws SerializationException { -+ final Class clazz = erase(target.getType()); -+ if (ConfigurationPart.class.isAssignableFrom(clazz)) { -+ final FieldDiscoverer.@Nullable InstanceFactory> instanceFactoryDelegate = this.delegate.discover(target, (name, type, annotations, deserializer, serializer) -> { -+ if (!erase(type.getType()).equals(clazz.getEnclosingClass())) { // don't collect synth fields for inner classes -+ collector.accept(name, type, annotations, deserializer, serializer); -+ } -+ }); -+ if (instanceFactoryDelegate instanceof FieldDiscoverer.MutableInstanceFactory> mutableInstanceFactoryDelegate) { -+ return new MutableInstanceFactory<>() { -+ @Override -+ public Map begin() { -+ return mutableInstanceFactoryDelegate.begin(); -+ } -+ -+ @SuppressWarnings("unchecked") -+ @Override -+ public void complete(Object instance, Map intermediate) throws SerializationException { -+ final Iterator> iter = intermediate.entrySet().iterator(); -+ try { -+ while (iter.hasNext()) { // manually merge any mergeable maps -+ Map.Entry entry = iter.next(); -+ if (entry.getKey().isAnnotationPresent(MergeMap.class) && Map.class.isAssignableFrom(entry.getKey().getType()) && intermediate.get(entry.getKey()) instanceof Map map) { -+ iter.remove(); -+ @Nullable Map existingMap = (Map) entry.getKey().get(instance); -+ if (existingMap != null) { -+ existingMap.putAll(map); -+ } else { -+ entry.getKey().set(instance, entry.getValue()); -+ } -+ } -+ } -+ } catch (final IllegalAccessException e) { -+ throw new SerializationException(target.getType(), e); -+ } -+ mutableInstanceFactoryDelegate.complete(instance, intermediate); -+ } -+ -+ @Override -+ public Object complete(Map intermediate) throws SerializationException { -+ @Nullable Object targetInstance = InnerClassFieldDiscoverer.this.instanceMap.get(GenericTypeReflector.erase(target.getType())); -+ if (targetInstance != null) { -+ this.complete(targetInstance, intermediate); -+ } else { -+ targetInstance = mutableInstanceFactoryDelegate.complete(intermediate); -+ } -+ if (targetInstance instanceof ConfigurationPart.Post post) { -+ post.postProcess(); -+ } -+ return targetInstance; -+ } -+ -+ @Override -+ public boolean canCreateInstances() { -+ return mutableInstanceFactoryDelegate.canCreateInstances(); -+ } -+ }; -+ } -+ } -+ return null; -+ } -+ -+ private Map, Object> overrides() { -+ return this.overrides; -+ } -+ -+ static FieldDiscoverer worldConfig(Configurations.ContextMap contextMap) { -+ final Map, Object> overrides = Map.of( -+ WorldConfiguration.class, new WorldConfiguration( -+ contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), -+ contextMap.require(Configurations.WORLD_KEY) -+ ) -+ ); -+ return new InnerClassFieldDiscoverer(overrides); -+ } -+ -+ static FieldDiscoverer globalConfig() { -+ return new InnerClassFieldDiscoverer(Collections.emptyMap()); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/configuration/MergeMap.java b/src/main/java/io/papermc/paper/configuration/MergeMap.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/configuration/MergeMap.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.configuration; -+ -+import java.lang.annotation.Documented; -+import java.lang.annotation.ElementType; -+import java.lang.annotation.Retention; -+import java.lang.annotation.RetentionPolicy; -+import java.lang.annotation.Target; -+ -+/** -+ * For use in maps inside {@link ConfigurationPart}s that have default keys that shouldn't be removed by users -+ *

-+ * Note that when the config is reloaded, the maps will be merged again, so make sure this map can't accumulate -+ * keys overtime. -+ */ -+@Documented -+@Target(ElementType.FIELD) -+@Retention(RetentionPolicy.RUNTIME) -+public @interface MergeMap { -+} diff --git a/src/main/java/io/papermc/paper/configuration/NestedSetting.java b/src/main/java/io/papermc/paper/configuration/NestedSetting.java new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 @@ -962,14 +802,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +import com.mojang.logging.LogUtils; +import io.leangen.geantyref.TypeToken; +import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; ++import io.papermc.paper.configuration.mapping.InnerClassFieldDiscoverer; +import io.papermc.paper.configuration.serializer.ComponentSerializer; +import io.papermc.paper.configuration.serializer.EnumValueSerializer; -+import io.papermc.paper.configuration.serializer.collections.FastutilMapSerializer; +import io.papermc.paper.configuration.serializer.NbtPathSerializer; +import io.papermc.paper.configuration.serializer.PacketClassSerializer; +import io.papermc.paper.configuration.serializer.StringRepresentableSerializer; -+import io.papermc.paper.configuration.serializer.collections.TableSerializer; ++import io.papermc.paper.configuration.serializer.collections.FastutilMapSerializer; +import io.papermc.paper.configuration.serializer.collections.MapSerializer; ++import io.papermc.paper.configuration.serializer.collections.TableSerializer; +import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer; +import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer; +import io.papermc.paper.configuration.transformation.Transformations; @@ -980,16 +821,25 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +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.type.BooleanOrDefault; -+import io.papermc.paper.configuration.type.DoubleOrDefault; +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.IntOr; ++import io.papermc.paper.configuration.type.number.DoubleOr; ++import io.papermc.paper.configuration.type.number.IntOr; +import io.papermc.paper.configuration.type.fallback.FallbackValueSerializer; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2LongMap; +import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap; ++import java.io.File; ++import java.io.IOException; ++import java.lang.reflect.Type; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.StandardCopyOption; ++import java.util.List; ++import java.util.function.Function; ++import java.util.function.Supplier; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; @@ -1014,16 +864,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +import org.spongepowered.configurate.transformation.TransformAction; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + -+import java.io.File; -+import java.io.IOException; -+import java.lang.reflect.Type; -+import java.nio.file.Files; -+import java.nio.file.Path; -+import java.nio.file.StandardCopyOption; -+import java.util.List; -+import java.util.function.Function; -+import java.util.function.Supplier; -+ +import static com.google.common.base.Preconditions.checkState; +import static io.leangen.geantyref.GenericTypeReflector.erase; + @@ -1093,7 +933,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + SpigotConfig.readConfig(SpigotWorldConfig.class, this); + } + }); -+ static final ContextKey> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken>() {}, "spigot world config"); ++ public static final ContextKey> SPIGOT_WORLD_CONFIG_CONTEXT_KEY = new ContextKey<>(new TypeToken>() {}, "spigot world config"); + + + public PaperConfigurations(final Path globalFolder) { @@ -1156,7 +996,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return super.createWorldObjectMapperFactoryBuilder(contextMap) + .addNodeResolver(new RequiresSpigotInitialization.Factory(contextMap.require(SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get())) + .addNodeResolver(new NestedSetting.Factory()) -+ .addDiscoverer(InnerClassFieldDiscoverer.worldConfig(contextMap)); ++ .addDiscoverer(InnerClassFieldDiscoverer.worldConfig(createWorldConfigInstance(contextMap))); ++ } ++ ++ private static WorldConfiguration createWorldConfigInstance(ContextMap contextMap) { ++ return new WorldConfiguration( ++ contextMap.require(PaperConfigurations.SPIGOT_WORLD_CONFIG_CONTEXT_KEY).get(), ++ contextMap.require(Configurations.WORLD_KEY) ++ ); + } + + @Override @@ -1171,7 +1018,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + .register(StringRepresentableSerializer::isValidFor, new StringRepresentableSerializer()) + .register(IntOr.Default.SERIALIZER) + .register(IntOr.Disabled.SERIALIZER) -+ .register(DoubleOrDefault.SERIALIZER) ++ .register(DoubleOr.Default.SERIALIZER) + .register(BooleanOrDefault.SERIALIZER) + .register(Duration.SERIALIZER) + .register(DurationOrDisabled.SERIALIZER) @@ -1499,21 +1346,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.mojang.logging.LogUtils; -+import io.papermc.paper.configuration.constraint.Constraint; -+import io.papermc.paper.configuration.constraint.Constraints; +import io.papermc.paper.configuration.legacy.MaxEntityCollisionsInitializer; +import io.papermc.paper.configuration.legacy.RequiresSpigotInitialization; +import io.papermc.paper.configuration.legacy.SpawnLoadedRangeInitializer; ++import io.papermc.paper.configuration.mapping.MergeMap; +import io.papermc.paper.configuration.serializer.NbtPathSerializer; +import io.papermc.paper.configuration.transformation.world.FeatureSeedsGeneration; +import io.papermc.paper.configuration.type.BooleanOrDefault; -+import io.papermc.paper.configuration.type.DoubleOrDefault; +import io.papermc.paper.configuration.type.Duration; +import io.papermc.paper.configuration.type.DurationOrDisabled; +import io.papermc.paper.configuration.type.EngineMode; -+import io.papermc.paper.configuration.type.IntOr; +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 it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2LongMap; @@ -1550,6 +1397,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 +import org.spigotmc.SpigotWorldConfig; +import org.spigotmc.TrackingRange; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; ++import org.spongepowered.configurate.objectmapping.meta.PostProcess; +import org.spongepowered.configurate.objectmapping.meta.Required; +import org.spongepowered.configurate.objectmapping.meta.Setting; + @@ -1695,11 +1543,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + + public boolean allChunksAreSlimeChunks = false; -+ @Constraint(Constraints.BelowZeroDoubleToDefault.class) -+ public DoubleOrDefault skeletonHorseThunderSpawnChance = DoubleOrDefault.USE_DEFAULT; ++ @BelowZeroToEmpty ++ public DoubleOr.Default skeletonHorseThunderSpawnChance = DoubleOr.Default.USE_DEFAULT; + public boolean ironGolemsCanSpawnInAir = false; + public boolean countAllMobsForSpawning = false; -+ public int monsterSpawnMaxLightLevel = -1; ++ @BelowZeroToEmpty ++ public IntOr.Default monsterSpawnMaxLightLevel = IntOr.Default.USE_DEFAULT; + public DuplicateUUID duplicateUuid; + + public class DuplicateUUID extends ConfigurationPart { @@ -1770,7 +1619,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + public int phantomsSpawnAttemptMinSeconds = 60; + public int phantomsSpawnAttemptMaxSeconds = 119; + public boolean parrotsAreUnaffectedByPlayerMovement = false; -+ public double zombieVillagerInfectionChance = -1.0; ++ @BelowZeroToEmpty ++ public DoubleOr.Default zombieVillagerInfectionChance = DoubleOr.Default.USE_DEFAULT; + public MobsCanAlwaysPickUpLoot mobsCanAlwaysPickUpLoot; + + public class MobsCanAlwaysPickUpLoot extends ConfigurationPart { @@ -1999,14 +1849,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + @Setting(FeatureSeedsGeneration.FEATURE_SEEDS_KEY) + public FeatureSeeds featureSeeds; + -+ public class FeatureSeeds extends ConfigurationPart.Post { ++ public class FeatureSeeds extends ConfigurationPart { ++ @SuppressWarnings("unused") // Is used in FeatureSeedsGeneration + @Setting(FeatureSeedsGeneration.GENERATE_KEY) + public boolean generateRandomSeedsForAll = false; + @Setting(FeatureSeedsGeneration.FEATURES_KEY) + public Reference2LongMap>> features = new Reference2LongOpenHashMap<>(); + -+ @Override -+ public void postProcess() { ++ @PostProcess ++ private void postProcess() { + this.features.defaultReturnValue(-1); + } + } @@ -2028,7 +1879,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + VANILLA, EIGENCRAFT, ALTERNATE_CURRENT + } + } -+ +} diff --git a/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java b/src/main/java/io/papermc/paper/configuration/constraint/Constraint.java new file mode 100644 @@ -2074,39 +1924,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @@ -0,0 +0,0 @@ +package io.papermc.paper.configuration.constraint; + -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.configuration.GlobalConfiguration; -+import io.papermc.paper.configuration.type.DoubleOrDefault; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.slf4j.Logger; -+import org.spongepowered.configurate.objectmapping.meta.Constraint; -+import org.spongepowered.configurate.serialize.SerializationException; -+ +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; -+import java.util.OptionalDouble; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.meta.Constraint; ++import org.spongepowered.configurate.serialize.SerializationException; + +public final class Constraints { + private Constraints() { + } + -+ public static final class Velocity implements Constraint { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ @Override -+ public void validate(final GlobalConfiguration.Proxies.@Nullable Velocity value) throws SerializationException { -+ if (value != null && value.enabled && value.secret.isEmpty()) { -+ LOGGER.error("Velocity is enabled, but no secret key was specified. A secret key is required. Disabling velocity..."); -+ value.enabled = false; -+ } -+ } -+ } -+ + public static final class Positive implements Constraint { + @Override + public void validate(@Nullable Number value) throws SerializationException { @@ -2116,18 +1947,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + } + -+ public static final class BelowZeroDoubleToDefault implements Constraint { -+ @Override -+ public void validate(final @Nullable DoubleOrDefault container) { -+ if (container != null) { -+ final OptionalDouble value = container.value(); -+ if (value.isPresent() && value.getAsDouble() < 0) { -+ container.value(OptionalDouble.empty()); -+ } -+ } -+ } -+ } -+ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) @@ -2271,6 +2090,241 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return node; + } +} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassFieldDiscoverer.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import io.papermc.paper.configuration.ConfigurationPart; ++import io.papermc.paper.configuration.Configurations; ++import io.papermc.paper.configuration.PaperConfigurations; ++import io.papermc.paper.configuration.WorldConfiguration; ++import java.lang.reflect.AnnotatedType; ++import java.lang.reflect.Field; ++import java.util.Collections; ++import java.util.Map; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.FieldDiscoverer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++public final class InnerClassFieldDiscoverer implements FieldDiscoverer> { ++ ++ private final InnerClassInstanceSupplier instanceSupplier; ++ private final FieldDiscoverer> delegate; ++ ++ @SuppressWarnings("unchecked") ++ public InnerClassFieldDiscoverer(final Map, Object> initialOverrides) { ++ this.instanceSupplier = new InnerClassInstanceSupplier(initialOverrides); ++ this.delegate = (FieldDiscoverer>) FieldDiscoverer.object(this.instanceSupplier); ++ } ++ ++ @Override ++ public @Nullable InstanceFactory> discover(final AnnotatedType target, final FieldCollector, V> collector) throws SerializationException { ++ final Class clazz = erase(target.getType()); ++ if (ConfigurationPart.class.isAssignableFrom(clazz)) { ++ final FieldDiscoverer.@Nullable InstanceFactory> instanceFactoryDelegate = this.delegate.discover(target, (name, type, annotations, deserializer, serializer) -> { ++ if (!erase(type.getType()).equals(clazz.getEnclosingClass())) { // don't collect synth fields for inner classes ++ collector.accept(name, type, annotations, deserializer, serializer); ++ } ++ }); ++ if (instanceFactoryDelegate instanceof MutableInstanceFactory> mutableInstanceFactoryDelegate) { ++ return new InnerClassInstanceFactory(this.instanceSupplier, mutableInstanceFactoryDelegate, target); ++ } ++ } ++ return null; ++ } ++ ++ public static FieldDiscoverer worldConfig(WorldConfiguration worldConfiguration) { ++ final Map, Object> overrides = Map.of( ++ WorldConfiguration.class, worldConfiguration ++ ); ++ return new InnerClassFieldDiscoverer(overrides); ++ } ++ ++ public static FieldDiscoverer globalConfig() { ++ return new InnerClassFieldDiscoverer(Collections.emptyMap()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceFactory.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import java.lang.reflect.AnnotatedType; ++import java.lang.reflect.Field; ++import java.util.Iterator; ++import java.util.Map; ++import java.util.Objects; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.objectmapping.FieldDiscoverer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++final class InnerClassInstanceFactory implements FieldDiscoverer.MutableInstanceFactory> { ++ ++ private final InnerClassInstanceSupplier instanceSupplier; ++ private final FieldDiscoverer.MutableInstanceFactory> fallback; ++ private final AnnotatedType targetType; ++ ++ InnerClassInstanceFactory(final InnerClassInstanceSupplier instanceSupplier, final FieldDiscoverer.MutableInstanceFactory> fallback, final AnnotatedType targetType) { ++ this.instanceSupplier = instanceSupplier; ++ this.fallback = fallback; ++ this.targetType = targetType; ++ } ++ ++ @Override ++ public Map begin() { ++ return this.fallback.begin(); ++ } ++ ++ @SuppressWarnings("unchecked") ++ @Override ++ public void complete(final Object instance, final Map intermediate) throws SerializationException { ++ final Iterator> iter = intermediate.entrySet().iterator(); ++ try { ++ while (iter.hasNext()) { // manually merge any mergeable maps ++ Map.Entry entry = iter.next(); ++ if (entry.getKey().isAnnotationPresent(MergeMap.class) && Map.class.isAssignableFrom(entry.getKey().getType()) && intermediate.get(entry.getKey()) instanceof Map map) { ++ iter.remove(); ++ @Nullable Map existingMap = (Map) entry.getKey().get(instance); ++ if (existingMap != null) { ++ existingMap.putAll(map); ++ } else { ++ entry.getKey().set(instance, entry.getValue()); ++ } ++ } ++ } ++ } catch (final IllegalAccessException e) { ++ throw new SerializationException(this.targetType.getType(), e); ++ } ++ this.fallback.complete(instance, intermediate); ++ } ++ ++ @Override ++ public Object complete(final Map intermediate) throws SerializationException { ++ final Object targetInstance = Objects.requireNonNull(this.instanceSupplier.instanceMap().get(erase(this.targetType.getType())), () -> this.targetType.getType() + " must already have an instance created"); ++ this.complete(targetInstance, intermediate); ++ return targetInstance; ++ } ++ ++ @Override ++ public boolean canCreateInstances() { ++ return this.fallback.canCreateInstances(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/InnerClassInstanceSupplier.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import io.papermc.paper.configuration.ConfigurationPart; ++import java.lang.reflect.AnnotatedType; ++import java.lang.reflect.Constructor; ++import java.lang.reflect.Modifier; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Supplier; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.spongepowered.configurate.serialize.SerializationException; ++import org.spongepowered.configurate.util.CheckedFunction; ++import org.spongepowered.configurate.util.CheckedSupplier; ++ ++import static io.leangen.geantyref.GenericTypeReflector.erase; ++ ++/** ++ * This instance factory handles creating non-static inner classes by tracking all instances of objects that extend ++ * {@link ConfigurationPart}. Only 1 instance of each {@link ConfigurationPart} should be present for each instance ++ * of the field discoverer this is used in. ++ */ ++final class InnerClassInstanceSupplier implements CheckedFunction, SerializationException> { ++ ++ private final Map, Object> instanceMap = new HashMap<>(); ++ private final Map, Object> initialOverrides; ++ ++ /** ++ * @param initialOverrides map of types to objects to preload the config objects with. ++ */ ++ InnerClassInstanceSupplier(final Map, Object> initialOverrides) { ++ this.initialOverrides = initialOverrides; ++ } ++ ++ @Override ++ public Supplier apply(final AnnotatedType target) throws SerializationException { ++ final Class type = erase(target.getType()); ++ if (this.initialOverrides.containsKey(type)) { ++ this.instanceMap.put(type, this.initialOverrides.get(type)); ++ return () -> this.initialOverrides.get(type); ++ } ++ if (ConfigurationPart.class.isAssignableFrom(type) && !this.instanceMap.containsKey(type)) { ++ try { ++ final Constructor constructor; ++ final CheckedSupplier instanceSupplier; ++ if (type.getEnclosingClass() != null && !Modifier.isStatic(type.getModifiers())) { ++ final @Nullable Object instance = this.instanceMap.get(type.getEnclosingClass()); ++ if (instance == null) { ++ throw new SerializationException("Cannot create a new instance of an inner class " + type.getName() + " without an instance of its enclosing class " + type.getEnclosingClass().getName()); ++ } ++ constructor = type.getDeclaredConstructor(type.getEnclosingClass()); ++ instanceSupplier = () -> constructor.newInstance(instance); ++ } else { ++ constructor = type.getDeclaredConstructor(); ++ instanceSupplier = constructor::newInstance; ++ } ++ constructor.setAccessible(true); ++ final Object instance = instanceSupplier.get(); ++ this.instanceMap.put(type, instance); ++ return () -> instance; ++ } catch (ReflectiveOperationException e) { ++ throw new SerializationException(ConfigurationPart.class, target + " must be a valid ConfigurationPart", e); ++ } ++ } else { ++ throw new SerializationException(target + " must be a valid ConfigurationPart"); ++ } ++ } ++ ++ Map, Object> instanceMap() { ++ return this.instanceMap; ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java b/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/mapping/MergeMap.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.mapping; ++ ++import io.papermc.paper.configuration.ConfigurationPart; ++import java.lang.annotation.Documented; ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++ ++/** ++ * For use in maps inside {@link ConfigurationPart}s that have default keys that shouldn't be removed by users ++ *

++ * Note that when the config is reloaded, the maps will be merged again, so make sure this map can't accumulate ++ * keys overtime. ++ */ ++@Documented ++@Target(ElementType.FIELD) ++@Retention(RetentionPolicy.RUNTIME) ++public @interface MergeMap { ++} diff --git a/src/main/java/io/papermc/paper/configuration/package-info.java b/src/main/java/io/papermc/paper/configuration/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 @@ -3845,7 +3899,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 @@ -0,0 +0,0 @@ +package io.papermc.paper.configuration.transformation.world.versioned; + -+import io.papermc.paper.configuration.type.IntOr; ++import io.papermc.paper.configuration.type.number.IntOr; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; @@ -3968,7 +4022,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } else if (obj instanceof Boolean bool) { + return new BooleanOrDefault(bool); + } -+ throw new SerializationException(obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'"); ++ throw new SerializationException(BooleanOrDefault.class, obj + "(" + type + ") is not a boolean or '" + DEFAULT_VALUE + "'"); + } + + @Override @@ -3982,77 +4036,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + } + } +} -diff --git a/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java b/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/configuration/type/DoubleOrDefault.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.configuration.type; -+ -+import org.apache.commons.lang3.math.NumberUtils; -+import org.spongepowered.configurate.serialize.ScalarSerializer; -+import org.spongepowered.configurate.serialize.SerializationException; -+ -+import java.lang.reflect.Type; -+import java.util.OptionalDouble; -+import java.util.function.Predicate; -+ -+@SuppressWarnings("OptionalUsedAsFieldOrParameterType") -+public final class DoubleOrDefault { -+ private static final String DEFAULT_VALUE = "default"; -+ public static final DoubleOrDefault USE_DEFAULT = new DoubleOrDefault(OptionalDouble.empty()); -+ public static final ScalarSerializer SERIALIZER = new Serializer(); -+ -+ private OptionalDouble value; -+ -+ public DoubleOrDefault(final OptionalDouble value) { -+ this.value = value; -+ } -+ -+ public OptionalDouble value() { -+ return this.value; -+ } -+ -+ public void value(final OptionalDouble value) { -+ this.value = value; -+ } -+ -+ public double or(final double fallback) { -+ return this.value.orElse(fallback); -+ } -+ -+ private static final class Serializer extends ScalarSerializer { -+ Serializer() { -+ super(DoubleOrDefault.class); -+ } -+ -+ @Override -+ public DoubleOrDefault deserialize(final Type type, final Object obj) throws SerializationException { -+ if (obj instanceof String string) { -+ if (DEFAULT_VALUE.equalsIgnoreCase(string)) { -+ return USE_DEFAULT; -+ } -+ if (NumberUtils.isParsable(string)) { -+ return new DoubleOrDefault(OptionalDouble.of(Double.parseDouble(string))); -+ } -+ } else if (obj instanceof Number num) { -+ return new DoubleOrDefault(OptionalDouble.of(num.doubleValue())); -+ } -+ throw new SerializationException(obj + "(" + type + ") is not a double or '" + DEFAULT_VALUE + "'"); -+ } -+ -+ @Override -+ protected Object serialize(final DoubleOrDefault item, final Predicate> typeSupported) { -+ final OptionalDouble value = item.value(); -+ if (value.isPresent()) { -+ return value.getAsDouble(); -+ } else { -+ return DEFAULT_VALUE; -+ } -+ } -+ } -+} diff --git a/src/main/java/io/papermc/paper/configuration/type/Duration.java b/src/main/java/io/papermc/paper/configuration/type/Duration.java new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 @@ -4259,101 +4242,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return description; + } +} -diff --git a/src/main/java/io/papermc/paper/configuration/type/IntOr.java b/src/main/java/io/papermc/paper/configuration/type/IntOr.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/configuration/type/IntOr.java -@@ -0,0 +0,0 @@ -+package io.papermc.paper.configuration.type; -+ -+import com.mojang.logging.LogUtils; -+import java.lang.reflect.Type; -+import java.util.OptionalInt; -+import java.util.function.Function; -+import java.util.function.IntPredicate; -+import java.util.function.Predicate; -+import org.apache.commons.lang3.math.NumberUtils; -+import org.slf4j.Logger; -+import org.spongepowered.configurate.serialize.ScalarSerializer; -+import org.spongepowered.configurate.serialize.SerializationException; -+ -+public interface IntOr { -+ -+ Logger LOGGER = LogUtils.getClassLogger(); -+ -+ default int or(final int fallback) { -+ return this.value().orElse(fallback); -+ } -+ -+ OptionalInt value(); -+ -+ default int intValue() { -+ return this.value().orElseThrow(); -+ } -+ -+ record Default(OptionalInt value) implements IntOr { -+ private static final String DEFAULT_VALUE = "default"; -+ public static final Default USE_DEFAULT = new Default(OptionalInt.empty()); -+ public static final ScalarSerializer SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT); -+ } -+ -+ record Disabled(OptionalInt value) implements IntOr { -+ private static final String DISABLED_VALUE = "disabled"; -+ public static final Disabled DISABLED = new Disabled(OptionalInt.empty()); -+ public static final ScalarSerializer SERIALIZER = new Serializer<>(Disabled.class, Disabled::new, DISABLED_VALUE, DISABLED); -+ -+ public boolean test(IntPredicate predicate) { -+ return this.value.isPresent() && predicate.test(this.value.getAsInt()); -+ } -+ -+ public boolean enabled() { -+ return this.value.isPresent(); -+ } -+ } -+ -+ final class Serializer extends ScalarSerializer { -+ -+ private final Function creator; -+ private final String otherSerializedValue; -+ private final T otherValue; -+ -+ public Serializer(Class classOfT, Function creator, String otherSerializedValue, T otherValue) { -+ super(classOfT); -+ this.creator = creator; -+ this.otherSerializedValue = otherSerializedValue; -+ this.otherValue = otherValue; -+ } -+ -+ @Override -+ public T deserialize(Type type, Object obj) throws SerializationException { -+ if (obj instanceof String string) { -+ if (this.otherSerializedValue.equalsIgnoreCase(string)) { -+ return this.otherValue; -+ } -+ if (NumberUtils.isParsable(string)) { -+ return this.creator.apply(OptionalInt.of(Integer.parseInt(string))); -+ } -+ } else if (obj instanceof Number num) { -+ if (num.intValue() != num.doubleValue() || num.intValue() != num.longValue()) { -+ LOGGER.error("{} cannot be converted to an integer without losing information", num); -+ } -+ return this.creator.apply(OptionalInt.of(num.intValue())); -+ } -+ throw new SerializationException(obj + "(" + type + ") is not a integer or '" + this.otherSerializedValue + "'"); -+ } -+ -+ @Override -+ protected Object serialize(T item, Predicate> typeSupported) { -+ final OptionalInt value = item.value(); -+ if (value.isPresent()) { -+ return value.getAsInt(); -+ } else { -+ return this.otherSerializedValue; -+ } -+ } -+ } -+} diff --git a/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java b/src/main/java/io/papermc/paper/configuration/type/fallback/ArrowDespawnRate.java new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 @@ -4628,6 +4516,237 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + return value < 0 ? OptionalInt.empty() : OptionalInt.of(value); + } +} +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java b/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/BelowZeroToEmpty.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import java.lang.annotation.ElementType; ++import java.lang.annotation.Retention; ++import java.lang.annotation.RetentionPolicy; ++import java.lang.annotation.Target; ++ ++@Retention(RetentionPolicy.RUNTIME) ++@Target(ElementType.FIELD) ++public @interface BelowZeroToEmpty { ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java b/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/DoubleOr.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import com.google.common.base.Preconditions; ++import java.util.OptionalDouble; ++import java.util.function.Function; ++import java.util.function.Predicate; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++ ++public interface DoubleOr { ++ ++ default double or(final double fallback) { ++ return this.value().orElse(fallback); ++ } ++ ++ OptionalDouble value(); ++ ++ default double doubleValue() { ++ return this.value().orElseThrow(); ++ } ++ ++ record Default(OptionalDouble value) implements DoubleOr { ++ private static final String DEFAULT_VALUE = "default"; ++ public static final Default USE_DEFAULT = new Default(OptionalDouble.empty()); ++ public static final ScalarSerializer SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT); ++ } ++ ++ final class Serializer extends OptionalNumSerializer { ++ Serializer(final Class classOfT, final Function factory, String emptySerializedValue, T emptyValue) { ++ super(classOfT, emptySerializedValue, emptyValue, OptionalDouble::empty, OptionalDouble::isEmpty, factory, double.class); ++ } ++ ++ @Override ++ protected Object serialize(final T item, final Predicate> typeSupported) { ++ final OptionalDouble value = item.value(); ++ if (value.isPresent()) { ++ return value.getAsDouble(); ++ } else { ++ return this.emptySerializedValue; ++ } ++ } ++ ++ @Override ++ protected OptionalDouble full(final String value) { ++ return OptionalDouble.of(Double.parseDouble(value)); ++ } ++ ++ @Override ++ protected OptionalDouble full(final Number num) { ++ return OptionalDouble.of(num.doubleValue()); ++ } ++ ++ @Override ++ protected boolean belowZero(final OptionalDouble value) { ++ Preconditions.checkArgument(value.isPresent()); ++ return value.getAsDouble() < 0; ++ } ++ } ++} ++ +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java b/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/IntOr.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import com.mojang.logging.LogUtils; ++import java.util.OptionalInt; ++import java.util.function.Function; ++import java.util.function.IntPredicate; ++import java.util.function.Predicate; ++import org.slf4j.Logger; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++ ++public interface IntOr { ++ ++ Logger LOGGER = LogUtils.getClassLogger(); ++ ++ default int or(final int fallback) { ++ return this.value().orElse(fallback); ++ } ++ ++ OptionalInt value(); ++ ++ default int intValue() { ++ return this.value().orElseThrow(); ++ } ++ ++ record Default(OptionalInt value) implements IntOr { ++ private static final String DEFAULT_VALUE = "default"; ++ public static final Default USE_DEFAULT = new Default(OptionalInt.empty()); ++ public static final ScalarSerializer SERIALIZER = new Serializer<>(Default.class, Default::new, DEFAULT_VALUE, USE_DEFAULT); ++ } ++ ++ record Disabled(OptionalInt value) implements IntOr { ++ private static final String DISABLED_VALUE = "disabled"; ++ public static final Disabled DISABLED = new Disabled(OptionalInt.empty()); ++ public static final ScalarSerializer SERIALIZER = new Serializer<>(Disabled.class, Disabled::new, DISABLED_VALUE, DISABLED); ++ ++ public boolean test(IntPredicate predicate) { ++ return this.value.isPresent() && predicate.test(this.value.getAsInt()); ++ } ++ ++ public boolean enabled() { ++ return this.value.isPresent(); ++ } ++ } ++ ++ final class Serializer extends OptionalNumSerializer { ++ ++ private Serializer(Class classOfT, Function factory, String emptySerializedValue, T emptyValue) { ++ super(classOfT, emptySerializedValue, emptyValue, OptionalInt::empty, OptionalInt::isEmpty, factory, int.class); ++ } ++ ++ @Override ++ protected OptionalInt full(final String value) { ++ return OptionalInt.of(Integer.parseInt(value)); ++ } ++ ++ @Override ++ protected OptionalInt full(final Number num) { ++ if (num.intValue() != num.doubleValue() || num.intValue() != num.longValue()) { ++ LOGGER.error("{} cannot be converted to an integer without losing information", num); ++ } ++ return OptionalInt.of(num.intValue()); ++ } ++ ++ @Override ++ protected boolean belowZero(final OptionalInt value) { ++ return false; ++ } ++ ++ @Override ++ protected Object serialize(final T item, final Predicate> typeSupported) { ++ final OptionalInt value = item.value(); ++ if (value.isPresent()) { ++ return value.getAsInt(); ++ } else { ++ return this.emptySerializedValue; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java b/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/configuration/type/number/OptionalNumSerializer.java +@@ -0,0 +0,0 @@ ++package io.papermc.paper.configuration.type.number; ++ ++import java.lang.reflect.AnnotatedType; ++import java.util.function.Function; ++import java.util.function.Predicate; ++import java.util.function.Supplier; ++import org.apache.commons.lang3.math.NumberUtils; ++import org.spongepowered.configurate.serialize.ScalarSerializer; ++import org.spongepowered.configurate.serialize.SerializationException; ++ ++public abstract class OptionalNumSerializer extends ScalarSerializer.Annotated { ++ ++ protected final String emptySerializedValue; ++ protected final T emptyValue; ++ private final Supplier empty; ++ private final Predicate isEmpty; ++ private final Function factory; ++ private final Class number; ++ ++ protected OptionalNumSerializer(final Class classOfT, final String emptySerializedValue, final T emptyValue, final Supplier empty, final Predicate isEmpty, final Function factory, final Class number) { ++ super(classOfT); ++ this.emptySerializedValue = emptySerializedValue; ++ this.emptyValue = emptyValue; ++ this.empty = empty; ++ this.isEmpty = isEmpty; ++ this.factory = factory; ++ this.number = number; ++ } ++ ++ @Override ++ public final T deserialize(final AnnotatedType type, final Object obj) throws SerializationException { ++ final O value; ++ if (obj instanceof String string) { ++ if (this.emptySerializedValue.equalsIgnoreCase(string)) { ++ value = this.empty.get(); ++ } else if (NumberUtils.isParsable(string)) { ++ value = this.full(string); ++ } else { ++ throw new SerializationException("%s (%s) is not a(n) %s or '%s'".formatted(obj, type, this.number.getSimpleName(), this.emptySerializedValue)); ++ } ++ } else if (obj instanceof Number num) { ++ value = this.full(num); ++ } else { ++ throw new SerializationException("%s (%s) is not a(n) %s or '%s'".formatted(obj, type, this.number.getSimpleName(), this.emptySerializedValue)); ++ } ++ if (this.isEmpty.test(value) || (type.isAnnotationPresent(BelowZeroToEmpty.class) && this.belowZero(value))) { ++ return this.emptyValue; ++ } else { ++ return this.factory.apply(value); ++ } ++ } ++ ++ protected abstract O full(final String value); ++ ++ protected abstract O full(final Number num); ++ ++ protected abstract boolean belowZero(O value); ++} diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/server/Main.java