diff --git a/leaf-server/minecraft-patches/features/0011-Move-random-tick-random.patch b/leaf-server/minecraft-patches/features/0011-Move-random-tick-random.patch
index cbfbfd7dcb..ecc391442a 100644
--- a/leaf-server/minecraft-patches/features/0011-Move-random-tick-random.patch
+++ b/leaf-server/minecraft-patches/features/0011-Move-random-tick-random.patch
@@ -19,7 +19,7 @@ require it to be initialized earlier. By moving it to the superclass, we
initialize it earlier, ensuring that it is available sooner.
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
-index 745ec876080331f27f3eabf466ff914d39c8d2c4..d9f97558c650c7a45e25780ef274eb92d48d797e 100644
+index d22b619eb47cd99b7d3d44bf0fa5c6278bbaaf6a..16a0f7ede6924fee0019c6b054b96a2eaf68677d 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -979,8 +979,6 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
diff --git a/leaf-server/minecraft-patches/features/0012-Optimize-random-calls-in-chunk-ticking.patch b/leaf-server/minecraft-patches/features/0012-Optimize-random-calls-in-chunk-ticking.patch
index 10a0f57291..0089c136c2 100644
--- a/leaf-server/minecraft-patches/features/0012-Optimize-random-calls-in-chunk-ticking.patch
+++ b/leaf-server/minecraft-patches/features/0012-Optimize-random-calls-in-chunk-ticking.patch
@@ -57,7 +57,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
-index d9f97558c650c7a45e25780ef274eb92d48d797e..d8eeba21a5392af3b0f943296c0f943c38d23fc3 100644
+index 16a0f7ede6924fee0019c6b054b96a2eaf68677d..7952d6836743b6ed88221503c905d5c0dfa56e82 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -1059,7 +1059,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
diff --git a/leaf-server/minecraft-patches/features/0017-Remove-lambda-from-ticking-guard.patch b/leaf-server/minecraft-patches/features/0017-Remove-lambda-from-ticking-guard.patch
index 5f57bf5513..6959966b9a 100644
--- a/leaf-server/minecraft-patches/features/0017-Remove-lambda-from-ticking-guard.patch
+++ b/leaf-server/minecraft-patches/features/0017-Remove-lambda-from-ticking-guard.patch
@@ -33,7 +33,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
-index d8eeba21a5392af3b0f943296c0f943c38d23fc3..aa54ee4700d7a13439987dfaad3095faf5bbf19e 100644
+index 7952d6836743b6ed88221503c905d5c0dfa56e82..a02889c57217422b6231b9708ed8f7cb423da27c 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -919,7 +919,19 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
diff --git a/leaf-server/minecraft-patches/features/0119-Matter-Secure-Seed.patch b/leaf-server/minecraft-patches/features/0119-Matter-Secure-Seed.patch
index cecfae7a42..ca63656bc9 100644
--- a/leaf-server/minecraft-patches/features/0119-Matter-Secure-Seed.patch
+++ b/leaf-server/minecraft-patches/features/0119-Matter-Secure-Seed.patch
@@ -45,7 +45,7 @@ index 98f07bacb2a94b368c1969dc0cfb11b72f6cea1a..6755281b1dedf2078e1623918985de77
}
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
-index bc08326c6b8e0657357f57f9bc47ef3c55f7744f..306ab28189d3272efbdaaf09444accf52f570fee 100644
+index 6638155dd34519592893bf992ad5050f30c39626..d8940a667cf9c8216dd67add38debc86c3e276c6 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -692,6 +692,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
diff --git a/leaf-server/minecraft-patches/features/0121-Faster-random-generator.patch b/leaf-server/minecraft-patches/features/0121-Faster-random-generator.patch
index b9a05f3eeb..c63e5a9106 100644
--- a/leaf-server/minecraft-patches/features/0121-Faster-random-generator.patch
+++ b/leaf-server/minecraft-patches/features/0121-Faster-random-generator.patch
@@ -27,7 +27,7 @@ index 6755281b1dedf2078e1623918985de772bb78dfe..aa30556730b3c924f72ccd8702a88c38
final ServerLevel world = this.level;
final int randomTickSpeed = world.getGameRules().get(GameRules.RANDOM_TICK_SPEED);
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
-index 306ab28189d3272efbdaaf09444accf52f570fee..0817fd8c0006272f1ed0bce560f1200324ff42ec 100644
+index d8940a667cf9c8216dd67add38debc86c3e276c6..72b03879dee9613423648dd2763a76a28f375704 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -998,7 +998,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -97,7 +97,7 @@ index 664c1b3008b62c533b8e3302baa994ed0c135c42..bce2fc4fa3fc56edb2a29e5ba695ac8f
static RandomSource createThreadLocalInstance(final long seed) {
return new SingleThreadedRandomSource(seed);
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
-index 7035d1ede6383c2016685cd2ca2f86e269f75c73..50c424b1d498d052bd27f37e855d0db3f780ee0b 100644
+index c2b845232253f3930ef90e8970b745c3dfea9bbf..4845fc18afed8229a2a4f00de1269145ceacdfe0 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -173,7 +173,7 @@ public abstract class Entity
diff --git a/leaf-server/minecraft-patches/features/0326-Leaf-config-changes.patch b/leaf-server/minecraft-patches/features/0326-Leaf-config-changes.patch
new file mode 100644
index 0000000000..6de0b55b4e
--- /dev/null
+++ b/leaf-server/minecraft-patches/features/0326-Leaf-config-changes.patch
@@ -0,0 +1,550 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com>
+Date: Wed, 12 Oct 2022 10:42:15 -0400
+Subject: [PATCH] Leaf config changes
+
+
+diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4290.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4290.java
+index e1b2447e3a8655b79deaef0163159e9681acb9a7..75c53c02dec32d8bc02e5a841e53319cd6df2d86 100644
+--- a/ca/spottedleaf/dataconverter/minecraft/versions/V4290.java
++++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4290.java
+@@ -254,7 +254,7 @@ public final class V4290 {
+ return ret;
+ }
+ } catch (final JsonParseException ex) {
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.invalidLegacyTextComponent) LOGGER.error("Failed to convert json to nbt: " + unparsedJson, ex); // Leaf - Do not log invalid flatten text component parse
++ if (org.dreeam.leaf.config.modules.misc.global.LogToConsole.invalidLegacyTextComponent) LOGGER.error("Failed to convert json to nbt: " + unparsedJson, ex); // Leaf - Do not log invalid flatten text component parse
+ }
+
+ return null;
+diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java
+index 4d5c38c0fb6d18026f84be5eccf87d78608decaa..c05103092c25b82e72cfe990969836cc32a66265 100644
+--- a/io/papermc/paper/entity/activation/ActivationRange.java
++++ b/io/papermc/paper/entity/activation/ActivationRange.java
+@@ -83,7 +83,7 @@ public final class ActivationRange {
+
+ // Gale start - variable entity wake-up duration
+ private static int getWakeUpDurationWithVariance(Entity entity, int wakeUpDuration) {
+- final double deviation = entity.level().galeConfig().gameplayMechanics.entityWakeUpDurationRatioStandardDeviation;
++ final double deviation = entity.level().leafConfig().entityWakeUpDuration.ratioStandardDeviation;
+ final org.dreeam.leaf.util.math.random.FasterRandomSource wakeUpDurationRandom = org.dreeam.leaf.util.math.random.FasterRandomSource.SHARED_INSTANCE;
+
+ if (deviation <= 0) {
+diff --git a/net/minecraft/network/chat/SignedMessageChain.java b/net/minecraft/network/chat/SignedMessageChain.java
+index 3aa85202ed8fc2f45ca37504834ad2dcc141507a..dfa19e677e554f5abf9e6a01314d18c9b343fa8e 100644
+--- a/net/minecraft/network/chat/SignedMessageChain.java
++++ b/net/minecraft/network/chat/SignedMessageChain.java
+@@ -49,7 +49,7 @@ public class SignedMessageChain {
+ throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.CHAIN_BROKEN);
+ }
+
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.verifyChatOrder && body.timeStamp().isBefore(SignedMessageChain.this.lastTimeStamp)) { // Gale - Pufferfish - make chat order verification configurable
++ if (org.dreeam.leaf.config.modules.network.global.ChatOrderVerification.enabled && body.timeStamp().isBefore(SignedMessageChain.this.lastTimeStamp)) { // Gale - Pufferfish - make chat order verification configurable
+ this.setChainBroken();
+ throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.OUT_OF_ORDER_CHAT, org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event causes
+ }
+@@ -61,7 +61,7 @@ public class SignedMessageChain {
+ throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.INVALID_SIGNATURE);
+ }
+
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.chat.expiredMessageWarning && unpacked.hasExpiredServer(Instant.now())) { // Gale - do not log expired message warnings
++ if (org.dreeam.leaf.config.modules.misc.global.Chat.expiredMessageWarning && unpacked.hasExpiredServer(Instant.now())) { // Gale - do not log expired message warnings
+ SignedMessageChain.LOGGER.warn("Received expired chat: '{}'. Is the client/server system time unsynchronized?", body.content());
+ }
+
+diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java
+index cd117cafbe9c3fe6bcfda09abe8afc8bd6629b66..c8d0b0b0fbbc17cc7c904a6f637727b80bbd9317 100644
+--- a/net/minecraft/server/Main.java
++++ b/net/minecraft/server/Main.java
+@@ -129,7 +129,7 @@ public class Main {
+ Thread.ofPlatform().daemon().name("DataFixers init thread").start(DataFixers::getDataFixer);
+ // Paper end - Perf: Init DataConverter asynchronously
+ Util.startTimerHackThread();
+- org.dreeam.leaf.config.ConfigModule.loadAfterBootstrap(); // Leaf - Leaf config - post load
++ org.dreeam.leaf.config.LeafConfig.loadAfterBootstrap(); // Leaf - Leaf config - post load
+ Path settingsFile = Paths.get("server.properties");
+ DedicatedServerSettings settings = new DedicatedServerSettings(options); // CraftBukkit - CLI argument support
+ settings.forceSave();
+diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
+index 3083f7da4bd04db8724f8e457559bef4469d7675..a9be9cbf19448c7012fc562d1bebe42a013770c1 100644
+--- a/net/minecraft/server/MinecraftServer.java
++++ b/net/minecraft/server/MinecraftServer.java
+@@ -699,6 +699,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(server.storageSource.getDimensionPath(dimension), dimension.identifier(), spigotConfig, server.registryAccess(), gameRules)), spigotConfig -> server.galeConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(server.storageSource.getDimensionPath(dimension), dimension.identifier(), spigotConfig, server.registryAccess(), gameRules)), executor); // Paper - create paper world configs // Paper - Anti-Xray - Pass executor // Gale - Gale configuration
++ super(
++ levelData,
++ dimension,
++ server.registryAccess(),
++ levelStem.type(),
++ false,
++ isDebug,
++ biomeZoomSeed,
++ server.getMaxChainedNeighborUpdates(),
++ loadedWorldData.bukkitName(),
++ gen,
++ biomeProvider,
++ env,
++ spigotConfig -> server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(server.storageSource.getDimensionPath(dimension), dimension.identifier(), spigotConfig, server.registryAccess(), gameRules)), // Paper - create paper world configs
++ spigotConfig -> new org.galemc.gale.configuration.GaleWorldConfiguration(spigotConfig, dimension.identifier()), // Gale - Gale configuration
++ spigotConfig -> org.dreeam.leaf.config.LeafConfig.initWorldConfig(server.storageSource.getDimensionPath(dimension)), // Leaf - per-world configuration
++ executor
++ );
+ this.weatherData = savedDataStorage.computeIfAbsent(WeatherData.TYPE);
+ this.weatherData.setLevel(this);
+ this.typeKey = typeKey;
+diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
+index 4030776c3d12f8e797720180e141322b5d2cc548..4e2562d81775b94e7b10f1b64fddde5a358f52ca 100644
+--- a/net/minecraft/server/level/ServerPlayer.java
++++ b/net/minecraft/server/level/ServerPlayer.java
+@@ -2403,7 +2403,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
+ public void crit(final Entity entity) {
+ // Gale start - MultiPaper - broadcast crit animations as the entity being critted
+ var level = this.level();
+- level.getChunkSource().sendToTrackingPlayersAndSelf(level.galeConfig().gameplayMechanics.fixes.broadcastCritAnimationsAsTheEntityBeingCritted ? entity : this, new ClientboundAnimatePacket(entity, ClientboundAnimatePacket.CRITICAL_HIT));
++ level.getChunkSource().sendToTrackingPlayersAndSelf(level.leafConfig().fixes.broadcastCritAnimationsAsTheEntityBeingCritted ? entity : this, new ClientboundAnimatePacket(entity, ClientboundAnimatePacket.CRITICAL_HIT));
+ // Gale end - MultiPaper - broadcast crit animations as the entity being critted
+ }
+
+@@ -2411,7 +2411,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
+ public void magicCrit(final Entity entity) {
+ // Gale start - MultiPaper - broadcast crit animations as the entity being critted
+ var level = this.level();
+- level.getChunkSource().sendToTrackingPlayersAndSelf(level.galeConfig().gameplayMechanics.fixes.broadcastCritAnimationsAsTheEntityBeingCritted ? entity : this, new ClientboundAnimatePacket(entity, ClientboundAnimatePacket.MAGIC_CRITICAL_HIT));
++ level.getChunkSource().sendToTrackingPlayersAndSelf(level.leafConfig().fixes.broadcastCritAnimationsAsTheEntityBeingCritted ? entity : this, new ClientboundAnimatePacket(entity, ClientboundAnimatePacket.MAGIC_CRITICAL_HIT));
+ // Gale end - MultiPaper - broadcast crit animations as the entity being critted
+ }
+
+diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java
+index 1049e948496fcbab5f3d5789ec1e975692309a36..96594a636d6c871b007e671daf290684d743936f 100644
+--- a/net/minecraft/server/level/WorldGenRegion.java
++++ b/net/minecraft/server/level/WorldGenRegion.java
+@@ -327,7 +327,7 @@ public class WorldGenRegion implements WorldGenLevel {
+ int chunkX = SectionPos.blockToSectionCoord(pos.getX());
+ int chunkZ = SectionPos.blockToSectionCoord(pos.getZ());
+ // Paper start - Buffer OOB setBlock calls
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.setBlockInFarChunk && !hasSetFarWarned) { // Gale - Purpur - do not log setBlock in far chunks
++ if (org.dreeam.leaf.config.modules.misc.global.LogToConsole.setBlockInFarChunk && !hasSetFarWarned) { // Gale - Purpur - do not log setBlock in far chunks
+ Util.logAndPauseIfInIde(
+ "Detected setBlock in a far chunk ["
+ + chunkX
+diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+index 1c0b616aa34e93e015267fc2415d00431ad494d7..5c7272f449b569d3cdefc296088808b9b94b37c4 100644
+--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+@@ -115,7 +115,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ long now = System.nanoTime();
+ io.papermc.paper.util.KeepAlive.PendingKeepAlive pending = this.keepAlive.pendingKeepAlives.peek();
+ // Gale start - Purpur - send multiple keep-alive packets
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.keepalive.sendMultiple) {
++ if (org.dreeam.leaf.config.modules.network.global.Keepalive.sendMultiple) {
+ if (this.keepAlivePending && !keepAlives.isEmpty() && keepAlives.contains(packet.getId())) {
+ int ping = (int) (Util.getMillis() - packet.getId());
+ int updatedLatency = (this.latency * 3 + ping) / 4;
+@@ -306,7 +306,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ Profiler.get().push("keepAlive");
+ long now = Util.getMillis();
+ // Gale start - Purpur - send multiple keep-alive packets
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.keepalive.sendMultiple) {
++ if (org.dreeam.leaf.config.modules.network.global.Keepalive.sendMultiple) {
+ if (this.checkIfClosed(now)) {
+ long currTime = System.nanoTime();
+ if ((currTime - this.keepAlive.lastKeepAliveTx) >= java.util.concurrent.TimeUnit.SECONDS.toNanos(1L)) { // 1 second
+diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index 118f4df4599768488d5fd86e97b0b024bea00365..c7eed64b8446dca61c134ed18283ceb12d6b52c9 100644
+--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -1359,12 +1359,12 @@ public class ServerGamePacketListenerImpl
+
+ @Override
+ public void handleEditBook(final ServerboundEditBookPacket packet) {
+- // Gale start - Pufferfish - make book writing configurable
++ // Leaf start - Pufferfish - make book writing configurable
+ final org.bukkit.craftbukkit.entity.CraftPlayer craftPlayer = this.player.getBukkitEntity();
+- if (!(org.galemc.gale.configuration.GaleGlobalConfiguration.get().gameplayMechanics.enableBookWriting || craftPlayer.hasPermission(org.bukkit.craftbukkit.util.permissions.CraftDefaultPermissions.writeBooks) || craftPlayer.hasPermission("pufferfish.usebooks"))) {
++ if (!(org.dreeam.leaf.config.modules.gameplay.global.BookWriting.enableBookWriting || craftPlayer.hasPermission(org.bukkit.craftbukkit.util.permissions.CraftDefaultPermissions.writeBooks) || craftPlayer.hasPermission("pufferfish.usebooks"))) {
+ return;
+ }
+- // Gale end - Pufferfish - make book writing configurable
++ // Leaf end - Pufferfish - make book writing configurable
+ // Paper start - Book size limits
+ final io.papermc.paper.configuration.type.number.IntOr.Disabled pageMax = io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.pageMax;
+ if (!this.cserver.isPrimaryThread() && pageMax.enabled()) {
+@@ -2745,7 +2745,7 @@ public class ServerGamePacketListenerImpl
+ // CraftBukkit start
+ String rawMessage = message.signedContent();
+ if (rawMessage.isEmpty()) {
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.chat.emptyMessageWarning) LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName()); // Gale - do not log empty message warnings
++ if (org.dreeam.leaf.config.modules.misc.global.Chat.emptyMessageWarning) LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName()); // Gale - do not log empty message warnings
+ } else if (this.getCraftPlayer().isConversing()) {
+ final String conversationInput = rawMessage;
+ this.server.processQueue.add(() -> ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput));
+diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+index 4bbf4cd3ca3d412b70e3a851c2873240baff13c6..77d0e507458ef2bdc4c863860322910e468efdb3 100644
+--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+@@ -106,7 +106,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
+ public void tickTimeout() {
+ // Paper end - login cookie API
+ // Gale start - make slow login timeout configurable
+- int slowLoginTimeout = org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.premiumAccountSlowLoginTimeout;
++ int slowLoginTimeout = org.dreeam.leaf.config.modules.network.global.PremiumAccountSlowLoginTimeout.ticks;
+ if (this.tick++ >= (slowLoginTimeout < 1 ? MAX_TICKS_BEFORE_LOGIN : slowLoginTimeout)) {
+ // Gale end - make slow login timeout configurable
+ this.disconnectAsync(Component.translatable("multiplayer.disconnect.slow_login")); // Paper
+@@ -139,7 +139,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
+ public void disconnect(final Component component) {
+ try {
+ // Gale start - Pufferfish - do not log disconnections with null id
+- if (!org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.nullIdDisconnections && this.authenticatedProfile != null && this.authenticatedProfile.id() == null) {
++ if (!org.dreeam.leaf.config.modules.misc.global.LogToConsole.nullIdDisconnections && this.authenticatedProfile != null && this.authenticatedProfile.id() == null) {
+ String reasonString = component.getString();
+ if ("Disconnected".equals(reasonString) || Component.translatable("multiplayer.disconnect.generic").getString().equals(reasonString)) {
+ return;
+diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
+index 8f451c9116d0e464d01e85449891c7270bb1b0d7..6ebd0a86a07ae0a21976614fe6d0084b11226a9d 100644
+--- a/net/minecraft/server/players/PlayerList.java
++++ b/net/minecraft/server/players/PlayerList.java
+@@ -450,7 +450,7 @@ public abstract class PlayerList {
+ }
+ // Paper end - Configurable player collision
+ org.purpurmc.purpur.task.BossBarTask.addToAll(player); // Purpur - Implement TPSBar
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.playerLoginLocations) { // Gale - JettPack - make logging login location configurable
++ if (org.dreeam.leaf.config.modules.misc.global.LogToConsole.playerLoginLocations) { // Gale - JettPack - make logging login location configurable
+ // CraftBukkit start - moved down
+ LOGGER.info(
+ "{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", // Paper - add world identifier
+@@ -1420,7 +1420,7 @@ public abstract class PlayerList {
+ public void broadcastChatMessage(final PlayerChatMessage message, final Predicate isFiltered, final @Nullable ServerPlayer senderPlayer, final ChatType.Bound chatType, final @Nullable Function unsignedFunction) {
+ // Paper end
+ boolean trusted = this.verifyChatTrusted(message);
+- this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), chatType, trusted || !org.dreeam.leaf.config.modules.network.ChatMessageSignature.enabled || !org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.chat.notSecureMarker ? null : "Not Secure"); // Paper // Gale - do not log Not Secure marker // Leaf - Mirai - Configurable chat message signatures
++ this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), chatType, trusted || !org.dreeam.leaf.config.modules.network.ChatMessageSignature.enabled || !org.dreeam.leaf.config.modules.misc.global.Chat.notSecureMarker ? null : "Not Secure"); // Paper // Gale - do not log Not Secure marker // Leaf - Mirai - Configurable chat message signatures
+ OutgoingChatMessage tracked = OutgoingChatMessage.create(message);
+ boolean wasFullyFiltered = false;
+
+diff --git a/net/minecraft/stats/ServerRecipeBook.java b/net/minecraft/stats/ServerRecipeBook.java
+index 95a50f25455ec86f7662e9abd039be17609ed1e4..1ed76c10465bc14b5c50b8394992a48478d64d1b 100644
+--- a/net/minecraft/stats/ServerRecipeBook.java
++++ b/net/minecraft/stats/ServerRecipeBook.java
+@@ -110,7 +110,7 @@ public class ServerRecipeBook extends RecipeBook {
+ ) {
+ for (ResourceKey> recipe : recipes) {
+ if (!validator.test(recipe)) {
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.unrecognizedRecipes) LOGGER.error("Tried to load unrecognized recipe: {} removed now.", recipe); // Gale - Purpur - do not log unrecognized recipes
++ if (org.dreeam.leaf.config.modules.misc.global.LogToConsole.unrecognizedRecipes) LOGGER.error("Tried to load unrecognized recipe: {} removed now.", recipe); // Gale - Purpur - do not log unrecognized recipes
+ } else {
+ recipeAddingMethod.accept(recipe);
+ }
+diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java
+index 1df6efebeda39097d75590c751ec635962b6e626..b9097219282a028e1c3ac02fb28b2bab4e7c3efa 100644
+--- a/net/minecraft/stats/ServerStatsCounter.java
++++ b/net/minecraft/stats/ServerStatsCounter.java
+@@ -127,7 +127,7 @@ public class ServerStatsCounter extends StatsCounter {
+ this.stats
+ .putAll(
+ STATS_CODEC.parse(data.get("stats").orElseEmptyMap())
+- .resultOrPartial(error -> {if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.invalidStatistics) LOGGER.error("Failed to parse statistics for {}: {}", this.file, error);}) // Gale - EMC - do not log invalid statistics
++ .resultOrPartial(error -> {if (org.dreeam.leaf.config.modules.misc.global.LogToConsole.invalidStatistics) LOGGER.error("Failed to parse statistics for {}: {}", this.file, error);}) // Gale - EMC - do not log invalid statistics
+ .orElse(Map.of())
+ );
+ }
+diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
+index 650a382301321196b5f547f500869f2906311260..7df5dd2be9315c118e8feb1011a876bdfae918e3 100644
+--- a/net/minecraft/world/entity/Entity.java
++++ b/net/minecraft/world/entity/Entity.java
+@@ -1039,7 +1039,7 @@ public abstract class Entity
+ if (!this.level().isClientSide()) {
+ // Gale start - Slice - hide flames on entities with fire resistance
+ if (this instanceof net.minecraft.world.entity.LivingEntity livingEntity) {
+- this.setSharedFlagOnFire(this.remainingFireTicks > 0 && (!this.level.galeConfig().gameplayMechanics.hideFlamesOnEntitiesWithFireResistance || !livingEntity.hasEffect(net.minecraft.world.effect.MobEffects.FIRE_RESISTANCE)));
++ this.setSharedFlagOnFire(this.remainingFireTicks > 0 && (!this.level.leafConfig().hideFlamesOnEntitiesWithFireResistance.enabled || !livingEntity.hasEffect(net.minecraft.world.effect.MobEffects.FIRE_RESISTANCE)));
+ } else {
+ // Gale end - Slice - hide flames on entities with fire resistance
+ this.setSharedFlagOnFire(this.remainingFireTicks > 0);
+diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
+index 14cf550c114df95fd43a5e002715c34e92be3c2a..e7b2ee8b275cb701490d865be72bdf0dd7164fa5 100644
+--- a/net/minecraft/world/entity/LivingEntity.java
++++ b/net/minecraft/world/entity/LivingEntity.java
+@@ -463,7 +463,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin
+ if (this.isAlive() && this.level() instanceof ServerLevel level) {
+ boolean isPlayer = this instanceof Player;
+ // Gale start - Pufferfish - reduce in wall checks
+- long checkStuckInWallInterval = this.level().galeConfig().smallOptimizations.reducedIntervals.checkStuckInWall;
++ long checkStuckInWallInterval = this.level().leafConfig().reducedIntervals.checkStuckInWall;
+ if ((checkStuckInWallInterval <= 1 || (tickCount % checkStuckInWallInterval == 0 && couldPossiblyBeHurt(1.0F))) && this.isInWall()) {
+ // Gale end - Pufferfish - reduce in wall checks
+ this.hurtServer(level, this.damageSources().inWall(), 1.0F);
+diff --git a/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java b/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
+index 42c4ec0446ef5f78fba953fcaebae911de658aa4..36acbcd056b09c5d97a93e65fc77b4e266e6f41f 100644
+--- a/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
++++ b/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java
+@@ -103,7 +103,7 @@ public class BehaviorUtils {
+ itemEntity.setDeltaMovement(throwVector);
+ // Gale start - EMC - reduce villager item re-pickup
+ if (thrower instanceof net.minecraft.world.entity.npc.villager.Villager) {
+- int repickupDelay = thrower.level().galeConfig().smallOptimizations.reducedIntervals.villagerItemRepickup;
++ int repickupDelay = thrower.level().leafConfig().reducedIntervals.villagerItemRepickup;
+
+ if (repickupDelay <= -1) {
+ itemEntity.setDefaultPickUpDelay();
+diff --git a/net/minecraft/world/entity/ai/goal/RandomStrollGoal.java b/net/minecraft/world/entity/ai/goal/RandomStrollGoal.java
+index bc8fdce935f26c4ddb5c1b7e95e6317816fb9e88..df5fcee117314cbccfab6c385cec1346c5e055de 100644
+--- a/net/minecraft/world/entity/ai/goal/RandomStrollGoal.java
++++ b/net/minecraft/world/entity/ai/goal/RandomStrollGoal.java
+@@ -50,7 +50,7 @@ public class RandomStrollGoal extends Goal {
+ }
+
+ Vec3 pos = this.getPosition();
+- if (pos == null || (!this.mob.level().galeConfig().gameplayMechanics.entitiesCanRandomStrollIntoNonTickingChunks && !((net.minecraft.server.level.ServerLevel) this.mob.level()).isPositionEntityTicking(net.minecraft.core.BlockPos.containing(pos)))) { // Gale - MultiPaper - prevent entities random strolling into non-ticking chunks
++ if (pos == null || (!this.mob.level().leafConfig().randomStrollIntoNonTickingChunks.enabled && !((net.minecraft.server.level.ServerLevel) this.mob.level()).isPositionEntityTicking(net.minecraft.core.BlockPos.containing(pos)))) { // Gale - MultiPaper - prevent entities random strolling into non-ticking chunks
+ return false;
+ }
+
+diff --git a/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java b/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java
+index 1b2edb6ebf41041bf1d3aabcfe8ea348c0dcd469..e364d82ac613d6bc5e3997537a8348538c1e0047 100644
+--- a/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java
++++ b/net/minecraft/world/entity/ai/goal/RangedBowAttackGoal.java
+@@ -121,7 +121,7 @@ public class RangedBowAttackGoal extends Go
+ this.mob.lookAt(target, 30.0F, 30.0F);
+ // Gale start - Purpur - fix MC-121706
+ }
+- if (!hasStrafingTime || this.mob.level().galeConfig().gameplayMechanics.fixes.mc121706) {
++ if (!hasStrafingTime || this.mob.level().leafConfig().fixes.mc121706) {
+ // Gale end - Purpur - fix MC-121706
+ this.mob.getLookControl().setLookAt(target, 30.0F, 30.0F);
+ }
+diff --git a/net/minecraft/world/entity/ai/sensing/Sensing.java b/net/minecraft/world/entity/ai/sensing/Sensing.java
+index fea91f2b1738ee82cc2da356851e9a91fa262d55..3a891ff9d466b2597545b8d91098c9bf4a23a086 100644
+--- a/net/minecraft/world/entity/ai/sensing/Sensing.java
++++ b/net/minecraft/world/entity/ai/sensing/Sensing.java
+@@ -20,7 +20,7 @@ public class Sensing {
+ public Sensing(final Mob mob) {
+ this.mob = mob;
+ // Gale start - Petal - reduce line of sight updates - expiring entity id lists
+- int updateLineOfSightInterval = org.galemc.gale.configuration.GaleGlobalConfiguration.get().smallOptimizations.reducedIntervals.updateEntityLineOfSight;
++ int updateLineOfSightInterval = org.dreeam.leaf.config.modules.opt.global.ReducedIntervals.updateEntityLineOfSight;
+ if (updateLineOfSightInterval <= 1) {
+ this.expiring = null;
+ } else {
+diff --git a/net/minecraft/world/entity/animal/fish/WaterAnimal.java b/net/minecraft/world/entity/animal/fish/WaterAnimal.java
+index 9bbeb92efe6eae57114792578c935242b4fd753f..3d26752e6ed23b5a43dad89434b6e7558e7394d3 100644
+--- a/net/minecraft/world/entity/animal/fish/WaterAnimal.java
++++ b/net/minecraft/world/entity/animal/fish/WaterAnimal.java
+@@ -81,7 +81,7 @@ public abstract class WaterAnimal extends PathfinderMob {
+ minSpawnLevel = level.getMinecraftWorld().paperConfig().entities.spawning.wateranimalSpawnHeight.minimum.or(minSpawnLevel);
+ // Paper end - Make water animal spawn height configurable
+ // Gale start - Purpur - fix MC-238526
+- boolean shouldFixMC238526 = spawnReason == EntitySpawnReason.SPAWNER && level.getMinecraftWorld().galeConfig().gameplayMechanics.fixes.mc238526;
++ boolean shouldFixMC238526 = spawnReason == EntitySpawnReason.SPAWNER && level.getMinecraftWorld().leafConfig().fixes.mc238526;
+ boolean isAllowedHeight = pos.getY() >= minSpawnLevel && pos.getY() <= seaLevel;
+ return (shouldFixMC238526 || isAllowedHeight)
+ // Gale end - Purpur - fix MC-238526
+diff --git a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
+index 571da17706bf21ef955117e9e2bbda24ed144bba..8f232847b3944607c2afefd06f98fe1ecd9c1003 100644
+--- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
++++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
+@@ -351,10 +351,10 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier {
+ return DoubleDoubleImmutablePair.of(dx, dz);
+ }
+
+- // Gale start - EMC - make saving fireworks configurable
++ // Leaf start - EMC - make saving fireworks configurable
+ @Override
+ public boolean shouldBeSaved() {
+- return this.level().galeConfig().smallOptimizations.saveFireworks && super.shouldBeSaved();
++ return this.level().leafConfig().saveFireworks.enabled && super.shouldBeSaved();
+ }
+- // Gale end - EMC - make saving fireworks configurable
++ // Leaf end - EMC - make saving fireworks configurable
+ }
+diff --git a/net/minecraft/world/entity/projectile/Projectile.java b/net/minecraft/world/entity/projectile/Projectile.java
+index 8f0113a7f74cc207fc9e3ebe487af58aa0e3424d..4446ed8a1a75315386886593e5113f8f76e3c041 100644
+--- a/net/minecraft/world/entity/projectile/Projectile.java
++++ b/net/minecraft/world/entity/projectile/Projectile.java
+@@ -72,19 +72,19 @@ public abstract class Projectile extends Entity implements TraceableEntity {
+ boolean isLoaded = ((net.minecraft.server.level.ServerChunkCache) this.level().getChunkSource()).getChunkAtIfLoadedImmediately(newX, newZ) != null;
+
+ if (!isLoaded) {
+- var maxProjectileChunkLoadsConfig = this.level().galeConfig().smallOptimizations.maxProjectileChunkLoads;
++ var maxProjectileChunkLoadsConfig = this.level().leafConfig().maxProjectileChunkLoads;
+ int maxChunkLoadsPerTick = maxProjectileChunkLoadsConfig.perTick;
+
+ if (maxChunkLoadsPerTick >= 0 && chunksLoadedThisTick > maxChunkLoadsPerTick) {
+ return;
+ }
+
+- int maxChunkLoadsPerProjectile = maxProjectileChunkLoadsConfig.perProjectile.max;
++ int maxChunkLoadsPerProjectile = maxProjectileChunkLoadsConfig.perProjectileMax;
+
+ if (maxChunkLoadsPerProjectile >= 0 && this.chunksLoadedByProjectile >= maxChunkLoadsPerProjectile) {
+- if (maxProjectileChunkLoadsConfig.perProjectile.removeFromWorldAfterReachLimit) {
++ if (maxProjectileChunkLoadsConfig.perProjectileRemoveFromWorldAfterReachLimit) {
+ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Leaf - Fix Pufferfish and Purpur patches - Purpur
+- } else if (maxProjectileChunkLoadsConfig.perProjectile.resetMovementAfterReachLimit) {
++ } else if (maxProjectileChunkLoadsConfig.perProjectileResetMovementAfterReachLimit) {
+ this.setDeltaMovement(0, this.getDeltaMovement().y, 0);
+ }
+
+diff --git a/net/minecraft/world/item/DyeColor.java b/net/minecraft/world/item/DyeColor.java
+index 35b14be6dc363f1727133e985a49ef262da88373..77c6c009e65d2e1ca7f2821b82bca5b8a4edb702 100644
+--- a/net/minecraft/world/item/DyeColor.java
++++ b/net/minecraft/world/item/DyeColor.java
+@@ -212,7 +212,7 @@ public enum DyeColor implements StringRepresentable {
+
+ public static DyeColor getMixedColor(final ServerLevel level, final DyeColor dyeColor1, final DyeColor dyeColor2) {
+ // Gale start - carpet-fixes - optimize sheep offspring color
+- if (level.galeConfig().smallOptimizations.useOptimizedSheepOffspringColor) {
++ if (level.leafConfig().optimizedSheepOffspringColor.enabled) {
+ DyeColor col = properDye(dyeColor1, dyeColor2);
+ if (col == null) col = level.simpleRandom.nextBoolean() ? dyeColor1 : dyeColor2;
+ return col;
+diff --git a/net/minecraft/world/item/EndCrystalItem.java b/net/minecraft/world/item/EndCrystalItem.java
+index b2b8efad9da71fddb1a1011c05b7d6c7fe71bffb..1675a20199068972be743251feeb783e0cc823d9 100644
+--- a/net/minecraft/world/item/EndCrystalItem.java
++++ b/net/minecraft/world/item/EndCrystalItem.java
+@@ -52,7 +52,7 @@ public class EndCrystalItem extends Item {
+ // CraftBukkit end
+ level.addFreshEntity(crystal);
+ level.gameEvent(context.getPlayer(), GameEvent.ENTITY_PLACE, above);
+- if (level.galeConfig().gameplayMechanics.tryRespawnEnderDragonAfterEndCrystalPlace) { // Gale - Pufferfish - make ender dragon respawn attempt after placing end crystals configurable
++ if (level.leafConfig().enderDragonRespawn.tryAfterEndCrystalPlace) { // Gale - Pufferfish - make ender dragon respawn attempt after placing end crystals configurable
+ EnderDragonFight fight = serverLevel.getDragonFight();
+ if (fight != null) {
+ fight.tryRespawn(above); // Paper - Perf: Do crystal-portal proximity check before entity lookup
+diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
+index dcc783fe5aa1b4790db5873e22b32ea4517d68df..435792ec17f7b1171825af22406a83fe21804452 100644
+--- a/net/minecraft/world/level/Level.java
++++ b/net/minecraft/world/level/Level.java
+@@ -179,6 +179,16 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
+ }
+ // Gale end - Gale configuration
+
++ // Leaf start - per-world configuration
++ private volatile org.dreeam.leaf.config.LeafWorldConfig leafConfig;
++ public org.dreeam.leaf.config.LeafWorldConfig leafConfig() {
++ return this.leafConfig;
++ }
++ public void setLeafConfig(org.dreeam.leaf.config.LeafWorldConfig leafConfig) {
++ this.leafConfig = leafConfig;
++ }
++ // Leaf end - per-world configuration
++
+ public final org.purpurmc.purpur.PurpurWorldConfig purpurConfig; // Purpur - Purpur config files
+ public final io.papermc.paper.redstone.RedstoneWireTurbo turbo; // Leaf - SparklyPaper - parallel world ticking - moved to world
+ public static @Nullable BlockPos lastPhysicsProblem; // Spigot
+@@ -895,6 +905,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
+ io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, // Paper - create paper world config
+ java.util.function.Function galeWorldConfigCreator, // Gale - Gale configuration
++ java.util.function.Function leafWorldConfigCreator, // Leaf - per-world configuration
+ java.util.concurrent.Executor executor // Paper - Anti-Xray
+ ) {
+ // Paper start - getblock optimisations - cache world height/sections
+@@ -911,6 +923,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
+ this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
+ this.purpurConfig = new org.purpurmc.purpur.PurpurWorldConfig(bukkitName, environment, worldKey); // Purpur - Purpur config files
+ this.galeConfig = galeWorldConfigCreator.apply(this.spigotConfig); // Gale - Gale configuration
++ this.leafConfig = leafWorldConfigCreator.apply(this.spigotConfig); // Leaf - per-world configuration
+ this.playerBreedingCooldowns = this.getNewBreedingCooldownCache(); // Purpur - Add adjustable breeding cooldown to config
+ this.generator = generator;
+ this.world = new CraftWorld((ServerLevel) this, worldKey, biomeProvider, environment);
+diff --git a/net/minecraft/world/level/levelgen/PhantomSpawner.java b/net/minecraft/world/level/levelgen/PhantomSpawner.java
+index ba9a21ab3188a8ff0681e7d368eb4d2161d2e2c7..a30bdbdfd77c31ce330bad6be956325654059e94 100644
+--- a/net/minecraft/world/level/levelgen/PhantomSpawner.java
++++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java
+@@ -54,7 +54,7 @@ public class PhantomSpawner implements CustomSpawner {
+ .south(-10 + random.nextInt(21));
+ // Gale start - MultiPaper - don't load chunks to spawn phantoms
+ BlockState blockState;
+- if (level.galeConfig().smallOptimizations.loadChunks.toSpawnPhantoms) {
++ if (level.leafConfig().loadChunks.toSpawnPhantoms) {
+ blockState = level.getBlockState(spawnPos);
+ } else {
+ blockState = level.getBlockStateIfLoaded(spawnPos);
diff --git a/leaf-server/paper-patches/features/0019-Including-5s-in-getTPS.patch b/leaf-server/paper-patches/features/0019-Including-5s-in-getTPS.patch
index 87e5da7e16..ef12d525b3 100644
--- a/leaf-server/paper-patches/features/0019-Including-5s-in-getTPS.patch
+++ b/leaf-server/paper-patches/features/0019-Including-5s-in-getTPS.patch
@@ -12,7 +12,7 @@ index c9dccaa61c6b216ae9c827b8eed5d7a21cccef15..59392b5a240ebee6517670e10e637ad0
@Override
public double[] getTPS() {
-+ if (org.dreeam.leaf.config.modules.misc.Including5sIngetTPS.enabled) return getTPSIncluding5SecondAverage(); // Leaf - Including 5s in getTPS()
++ if (org.dreeam.leaf.config.modules.misc.Including5sInGetTPS.enabled) return getTPSIncluding5SecondAverage(); // Leaf - Including 5s in getTPS()
return this.getServer().getTPS();
}
diff --git a/leaf-server/paper-patches/features/0068-Leaf-config-changes.patch b/leaf-server/paper-patches/features/0068-Leaf-config-changes.patch
new file mode 100644
index 0000000000..600dac7988
--- /dev/null
+++ b/leaf-server/paper-patches/features/0068-Leaf-config-changes.patch
@@ -0,0 +1,141 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com>
+Date: Sat, 15 Aug 2026 09:05:57 +0800
+Subject: [PATCH] Leaf config changes
+
+
+diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+index 77ba1fb52b291a9b205cdf0e571c7de03fa24686..be5b329aa6c50eda9e574a97e9018c4f4ce35413 100644
+--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
++++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+@@ -317,7 +317,7 @@ public final class ChatProcessor {
+
+ private void sendToServer(final ChatType.Bound chatType, final @Nullable Function msgFunction) {
+ final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console));
+- ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, !org.dreeam.leaf.config.modules.network.ChatMessageSignature.enabled || !org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.chat.notSecureMarker || ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure"); // Gale - do not log Not Secure marker // Leaf - Mirai - Configurable chat message signatures
++ ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, !org.dreeam.leaf.config.modules.network.ChatMessageSignature.enabled || !org.dreeam.leaf.config.modules.misc.global.Chat.notSecureMarker || ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure"); // Gale - do not log Not Secure marker // Leaf - Mirai - Configurable chat message signatures
+ }
+ }
+
+diff --git a/src/main/java/io/papermc/paper/world/migration/LegacyCraftBukkitWorldMigration.java b/src/main/java/io/papermc/paper/world/migration/LegacyCraftBukkitWorldMigration.java
+index ba24fda2395f07cdb3d1c0c66811dfd4a5b81533..f8b8fb411a65b6b06f24aec1470eee1a71f1f433 100644
+--- a/src/main/java/io/papermc/paper/world/migration/LegacyCraftBukkitWorldMigration.java
++++ b/src/main/java/io/papermc/paper/world/migration/LegacyCraftBukkitWorldMigration.java
+@@ -83,6 +83,7 @@ final class LegacyCraftBukkitWorldMigration {
+ }
+
+ WorldMigrationSupport.migratePaperWorldConfig(this.sourceRoot, this.targetDimensionPath);
++ WorldMigrationSupport.migrateGaleWorldConfig(this.sourceRoot, this.targetDimensionPath); // Leaf - migrate legacy Gale world config
+
+ final var levelDataResult = WorldMigrationSupport.readFixedLevelData(sourceAccess);
+ if (levelDataResult.fatalError()) {
+@@ -163,7 +164,9 @@ final class LegacyCraftBukkitWorldMigration {
+ if (Files.isDirectory(root.resolve(LevelResource.DATA.id()))) {
+ return true;
+ }
+- if (Files.isRegularFile(root.resolve(WorldMigrationSupport.PAPER_WORLD_CONFIG)) || Files.isRegularFile(root.resolve(WorldMigrationSupport.LEGACY_UID_FILE_NAME))) {
++ if (Files.isRegularFile(root.resolve(WorldMigrationSupport.PAPER_WORLD_CONFIG))
++ || Files.isRegularFile(root.resolve(WorldMigrationSupport.GALE_WORLD_CONFIG)) // Leaf - preserve legacy Gale world config
++ || Files.isRegularFile(root.resolve(WorldMigrationSupport.LEGACY_UID_FILE_NAME))) {
+ return true;
+ }
+ for (final String directory : WorldMigrationSupport.DIMENSION_DIRECTORIES) {
+diff --git a/src/main/java/io/papermc/paper/world/migration/VanillaWorldMigration.java b/src/main/java/io/papermc/paper/world/migration/VanillaWorldMigration.java
+index 00ab529b50f3cd8b5e0b3256d500e00066ade762..18b5ee30d419e1c275eec67c24b6364914cc7437 100644
+--- a/src/main/java/io/papermc/paper/world/migration/VanillaWorldMigration.java
++++ b/src/main/java/io/papermc/paper/world/migration/VanillaWorldMigration.java
+@@ -46,6 +46,7 @@ final class VanillaWorldMigration {
+
+ if (rootOwnsThisWorld) {
+ WorldMigrationSupport.migratePaperWorldConfig(context.baseRoot(), context.targetDimensionPath());
++ WorldMigrationSupport.migrateGaleWorldConfig(context.baseRoot(), context.targetDimensionPath()); // Leaf - migrate legacy Gale world config
+ migrateLegacyWorldMetadata(context);
+ }
+
+diff --git a/src/main/java/io/papermc/paper/world/migration/WorldMigrationSupport.java b/src/main/java/io/papermc/paper/world/migration/WorldMigrationSupport.java
+index f961539a3e38b8fedc882aa8ee66766193ac4ccb..d3c47277decb82b6f7bd47663c10674481b0ff59 100644
+--- a/src/main/java/io/papermc/paper/world/migration/WorldMigrationSupport.java
++++ b/src/main/java/io/papermc/paper/world/migration/WorldMigrationSupport.java
+@@ -29,6 +29,7 @@ final class WorldMigrationSupport {
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+ static final List DIMENSION_DIRECTORIES = List.of("region", "entities", "poi");
+ static final String PAPER_WORLD_CONFIG = "paper-world.yml";
++ static final String GALE_WORLD_CONFIG = "gale-world.yml";
+ static final String LEGACY_UID_FILE_NAME = "uid.dat";
+
+ private WorldMigrationSupport() {
+@@ -68,18 +69,26 @@ final class WorldMigrationSupport {
+ }
+
+ static void migratePaperWorldConfig(final Path sourceRoot, final Path targetDimensionPath) throws IOException {
+- final Path source = sourceRoot.resolve(PAPER_WORLD_CONFIG);
++ migrateWorldConfig(sourceRoot, targetDimensionPath, PAPER_WORLD_CONFIG, "Paper");
++ }
++
++ static void migrateGaleWorldConfig(final Path sourceRoot, final Path targetDimensionPath) throws IOException {
++ migrateWorldConfig(sourceRoot, targetDimensionPath, GALE_WORLD_CONFIG, "Gale");
++ }
++
++ private static void migrateWorldConfig(final Path sourceRoot, final Path targetDimensionPath, final String fileName, final String configName) throws IOException {
++ final Path source = sourceRoot.resolve(fileName);
+ if (!Files.isRegularFile(source)) {
+ return;
+ }
+
+- final Path target = targetDimensionPath.resolve(PAPER_WORLD_CONFIG);
++ final Path target = targetDimensionPath.resolve(fileName);
+ if (Files.exists(target)) {
+ return;
+ }
+
+ Files.createDirectories(target.getParent());
+- LOGGER.info("Migrating Paper world config from {} to {}", source, target);
++ LOGGER.info("Migrating {} world config from {} to {}", configName, source, target);
+ Files.move(source, target);
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 22e244d63d13dffc1efa091ac8853a4fb224a958..7eecca5538e48f238b3d1d956501532fefba6b21 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -1005,7 +1005,6 @@ public final class CraftServer implements Server {
+ org.spigotmc.SpigotConfig.init((File) this.console.options.valueOf("spigot-settings")); // Spigot
+ this.console.paperConfigurations.reloadConfigs(this.console);
+ org.purpurmc.purpur.PurpurConfig.init((File) console.options.valueOf("purpur-settings")); // Purpur - Purpur config files
+- this.console.galeConfigurations.reloadConfigs(this.console); // Gale - Gale configuration
+ 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))
+diff --git a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java
+index 0efca0e1de2d6e5d9bc33c46174973758c8cabb3..5bd6d0062f09a23bc8562ac313816143e2abc8a9 100644
+--- a/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java
++++ b/src/main/java/org/bukkit/craftbukkit/legacy/CraftLegacy.java
+@@ -260,7 +260,7 @@ public final class CraftLegacy {
+ }
+
+ static {
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.legacyMaterialInitialization) LOGGER.warn("Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug!"); // Paper - Improve logging and errors; doesn't need to be an error // Gale - Purpur - do not log legacy Material initialization
++ if (org.dreeam.leaf.config.modules.misc.global.LogToConsole.legacyMaterialInitialization) LOGGER.warn("Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug!"); // Paper - Improve logging and errors; doesn't need to be an error // Gale - Purpur - do not log legacy Material initialization
+ if (MinecraftServer.getServer() != null && MinecraftServer.getServer().isDebugging()) {
+ new Exception().printStackTrace();
+ }
+diff --git a/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/src/main/java/org/spigotmc/TicksPerSecondCommand.java
+index 5a6347c8a7909ab25c302fdc9fd1e8a615ebe488..db168a9bde8fa447d50edd25f97383e93807b7b1 100644
+--- a/src/main/java/org/spigotmc/TicksPerSecondCommand.java
++++ b/src/main/java/org/spigotmc/TicksPerSecondCommand.java
+@@ -51,13 +51,13 @@ public class TicksPerSecondCommand extends Command {
+ builder.append(Component.join(JoinConfiguration.commas(true), tpsAvg));
+ sender.sendMessage(builder.asComponent());
+ // Gale start - YAPFA - last tick time - in TPS command
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.lastTickTimeInTpsCommand.enabled) {
++ if (org.dreeam.leaf.config.modules.misc.global.LastTickTimeInTpsCommand.enabled) {
+ long lastTickProperTime = net.minecraft.server.MinecraftServer.lastTickProperTime;
+ long lastTickOversleepTime = net.minecraft.server.MinecraftServer.lastTickOversleepTime;
+ var lastTickTimeMessage = text("Last tick: ")
+ .append(formatTickTimeDuration(lastTickProperTime, 44, 50, 51));
+
+- if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.lastTickTimeInTpsCommand.addOversleep) {
++ if (org.dreeam.leaf.config.modules.misc.global.LastTickTimeInTpsCommand.addOversleep) {
+ lastTickTimeMessage = lastTickTimeMessage.append(text(" self + "))
+ .append(formatTickTimeDuration(lastTickOversleepTime, Math.max(1, 51 - lastTickProperTime), Math.max(2, 52 - lastTickProperTime), Math.max(3, 53 - lastTickProperTime)))
+ .append(text(" oversleep = "))
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java b/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java
index 1caa996fef..3d6f87bbe4 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java
@@ -78,15 +78,15 @@ private static int getCorePoolSize() {
}
private static int getMaxPoolSize() {
- return AsyncPathfinding.asyncPathfindingMaxThreads;
+ return AsyncPathfinding.maxThreads;
}
private static long getKeepAliveTime() {
- return AsyncPathfinding.asyncPathfindingKeepalive;
+ return AsyncPathfinding.keepalive;
}
private static BlockingQueue getQueueImpl() {
- final int queueCapacity = AsyncPathfinding.asyncPathfindingQueueSize;
+ final int queueCapacity = AsyncPathfinding.queueSize;
return new LinkedBlockingQueue<>(queueCapacity);
}
@@ -103,7 +103,7 @@ private static RejectedExecutionHandler getRejectedPolicy() {
return (Runnable rejectedTask, ThreadPoolExecutor executor) -> {
BlockingQueue workQueue = executor.getQueue();
if (!executor.isShutdown()) {
- switch (AsyncPathfinding.asyncPathfindingRejectPolicy) {
+ switch (AsyncPathfinding.rejectPolicyType) {
case FLUSH_ALL -> {
if (!workQueue.isEmpty()) {
List pendingTasks = new ArrayList<>(workQueue.size());
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/path/PathfindTaskRejectPolicy.java b/leaf-server/src/main/java/org/dreeam/leaf/async/path/PathfindTaskRejectPolicy.java
index 4e195f440e..c69186f7c6 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/async/path/PathfindTaskRejectPolicy.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/async/path/PathfindTaskRejectPolicy.java
@@ -12,8 +12,8 @@ public static PathfindTaskRejectPolicy fromString(String policy) {
try {
return PathfindTaskRejectPolicy.valueOf(policy.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
- LeafConfig.LOGGER.warn("Invalid pathfind task reject policy: {}, falling back to {}.", policy, FLUSH_ALL.toString());
- return FLUSH_ALL;
+ LeafConfig.LOGGER.warn("Invalid pathfind task reject policy: {}, falling back to {}.", policy, CALLER_RUNS.toString());
+ return CALLER_RUNS;
}
}
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/command/subcommands/ReloadCommand.java b/leaf-server/src/main/java/org/dreeam/leaf/command/subcommands/ReloadCommand.java
index 978e33bbca..7eb07ca5e0 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/command/subcommands/ReloadCommand.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/command/subcommands/ReloadCommand.java
@@ -2,13 +2,11 @@
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
-import net.minecraft.server.MinecraftServer;
import org.dreeam.leaf.command.LeafCommand;
import org.dreeam.leaf.command.PermissionedLeafSubcommand;
import org.dreeam.leaf.config.LeafConfig;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
-import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.permissions.PermissionDefault;
public final class ReloadCommand extends PermissionedLeafSubcommand {
@@ -22,23 +20,10 @@ public ReloadCommand() {
@Override
public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
- this.doGaleReload(sender);
this.doLeafReload(sender);
return true;
}
- // Gale start - Gale commands - /gale reload command
- private void doGaleReload(final CommandSender sender) {
- Command.broadcastCommandMessage(sender, Component.text("Reloading Gale config...", NamedTextColor.GREEN));
-
- MinecraftServer server = ((CraftServer) sender.getServer()).getServer();
- server.galeConfigurations.reloadConfigs(server);
- server.server.reloadCount++;
-
- Command.broadcastCommandMessage(sender, Component.text("Gale config reload complete.", NamedTextColor.GREEN));
- }
- // Gale end - Gale commands - /gale reload command
-
private void doLeafReload(final CommandSender sender) {
Command.broadcastCommandMessage(sender, Component.text("Reloading Leaf config...", NamedTextColor.GREEN));
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigBinder.java b/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigBinder.java
new file mode 100644
index 0000000000..3cd0bbdd67
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigBinder.java
@@ -0,0 +1,342 @@
+package org.dreeam.leaf.config;
+
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+import org.dreeam.leaf.config.annotations.DoNotLoad;
+import org.dreeam.leaf.config.annotations.HotReloadUnsupported;
+import org.dreeam.leaf.config.util.ConfigPaths;
+import org.jspecify.annotations.Nullable;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Binds annotation-driven module fields to a global or world configuration view.
+ */
+final class ConfigBinder {
+
+ // Static fields no longer expose their code defaults after the first successful bind.
+ private static final Map GLOBAL_DEFAULT_VALUES = new HashMap<>();
+
+ static void registerGlobalDefaults(Object module) throws IllegalAccessException {
+ Class> moduleClass = module.getClass();
+ for (Field field : moduleClass.getDeclaredFields()) {
+ if (field.getAnnotation(ConfigInfo.class) == null
+ || field.isAnnotationPresent(DoNotLoad.class)) {
+ continue;
+ }
+
+ validateField(moduleClass, field, true);
+ field.setAccessible(true);
+ Object defaultValue = field.get(null);
+ if (defaultValue == null) {
+ throw new IllegalStateException("Configuration field has a null default value: " + field);
+ }
+ GLOBAL_DEFAULT_VALUES.putIfAbsent(field, copyValue(defaultValue));
+ }
+ }
+
+ static void collectGlobalReload(
+ Object module,
+ LeafConfigAccessor config,
+ List pendingValues
+ ) throws IllegalAccessException {
+ Class> moduleClass = module.getClass();
+ ConfigClassInfo classInfo = moduleClass.getAnnotation(ConfigClassInfo.class);
+ if (classInfo == null) {
+ throw new IllegalStateException("Configuration module " + moduleClass.getName()
+ + " is missing @ConfigClassInfo");
+ }
+
+ String basePath = ConfigPaths.modulePath(moduleClass);
+ String sectionComment = config.pickStringRegionBased(classInfo.comments());
+ if (sectionComment != null) {
+ config.addComment(basePath, sectionComment);
+ }
+
+ boolean skipModuleReload = moduleClass.isAnnotationPresent(HotReloadUnsupported.class);
+
+ for (Field field : moduleClass.getDeclaredFields()) {
+ ConfigInfo configInfo = field.getAnnotation(ConfigInfo.class);
+ if (configInfo == null
+ || field.isAnnotationPresent(DoNotLoad.class)) {
+ continue;
+ }
+
+ validateField(moduleClass, field, true);
+ field.setAccessible(true);
+
+ String path = ConfigPaths.fieldPath(moduleClass, field);
+ Object defaultValue = globalDefaultValue(field);
+
+ String comment = config.pickStringRegionBased(configInfo.comments());
+ Object loadedValue = readValue(config, path, comment, field, defaultValue, true);
+ if (!skipModuleReload && !field.isAnnotationPresent(HotReloadUnsupported.class)) {
+ pendingValues.add(new PendingValue(null, field, loadedValue));
+ }
+ }
+ }
+
+ static void collectWorldReload(
+ Object module,
+ Object loadedModule,
+ List pendingValues
+ ) throws IllegalAccessException {
+ Class> moduleClass = module.getClass();
+ if (loadedModule.getClass() != moduleClass) {
+ throw new IllegalArgumentException("Configuration modules must have the same type");
+ }
+ if (moduleClass.isAnnotationPresent(HotReloadUnsupported.class)) {
+ return;
+ }
+
+ for (Field field : moduleClass.getDeclaredFields()) {
+ if (field.getAnnotation(ConfigInfo.class) == null
+ || field.isAnnotationPresent(DoNotLoad.class)
+ || field.isAnnotationPresent(HotReloadUnsupported.class)) {
+ continue;
+ }
+
+ validateField(moduleClass, field, false);
+ field.setAccessible(true);
+ pendingValues.add(new PendingValue(module, field, field.get(loadedModule)));
+ }
+ }
+
+ public static void bind(
+ Object module,
+ @Nullable Object worldDefaultModule,
+ LeafConfigAccessor config,
+ boolean global
+ ) throws IllegalAccessException {
+ Class> moduleClass = module.getClass();
+ ConfigClassInfo classInfo = moduleClass.getAnnotation(ConfigClassInfo.class);
+ if (classInfo == null) {
+ throw new IllegalStateException("Configuration module " + moduleClass.getName()
+ + " is missing @ConfigClassInfo");
+ }
+
+ String basePath = ConfigPaths.modulePath(moduleClass);
+ String sectionComment = config.pickStringRegionBased(classInfo.comments());
+ if (sectionComment != null && (!(config instanceof LeafWorldConfig worldConfig) || worldConfig.isWorldDefaults())) {
+ config.addComment(basePath, sectionComment);
+ }
+
+ for (Field field : moduleClass.getDeclaredFields()) {
+ boolean skipLoad = field.getAnnotation(DoNotLoad.class) != null;
+ ConfigInfo configInfo = field.getAnnotation(ConfigInfo.class);
+
+ if (skipLoad || configInfo == null) {
+ continue;
+ }
+
+ if (global) {
+ bindGlobal(moduleClass, field, configInfo, config);
+ } else {
+ bindWorld(module, worldDefaultModule, moduleClass, field, configInfo, config);
+ }
+ }
+ }
+
+ private static void bindGlobal(
+ Class> moduleClass,
+ Field field,
+ ConfigInfo configInfo,
+ LeafConfigAccessor config
+ ) throws IllegalAccessException {
+ validateField(moduleClass, field, true);
+ field.setAccessible(true);
+
+ String path = ConfigPaths.fieldPath(moduleClass, field);
+ Object defaultValue = globalDefaultValue(field);
+
+ String comment = config.pickStringRegionBased(configInfo.comments());
+ Object loadedValue = readValue(config, path, comment, field, defaultValue, true);
+ field.set(null, loadedValue);
+ }
+
+ private static void bindWorld(
+ Object module,
+ @Nullable Object defaultsModule,
+ Class> moduleClass,
+ Field field,
+ ConfigInfo configInfo,
+ LeafConfigAccessor config
+ ) throws IllegalAccessException {
+ validateField(moduleClass, field, false);
+ field.setAccessible(true);
+
+ String path = ConfigPaths.fieldPath(moduleClass, field);
+ Object defaultValue = defaultsModule == null
+ ? field.get(module) // World default
+ : copyValue(field.get(defaultsModule)); // World override copy from defaults
+ if (defaultValue == null) {
+ throw new IllegalStateException("Configuration field has a null default value: " + field);
+ }
+
+ boolean worldOverridden = defaultsModule != null;
+ if (worldOverridden && !config.contains(path)) {
+ // Use world default if no override path defined
+ field.set(module, defaultValue);
+ return;
+ }
+
+ String comment = config.pickStringRegionBased(configInfo.comments());
+ Object loadedValue = readValue(config, path, comment, field, defaultValue, !worldOverridden);
+ field.set(module, loadedValue);
+ }
+
+ static void applyWorldDefaults(
+ Object module,
+ Object defaultsModule
+ ) throws IllegalAccessException {
+ Class> moduleClass = module.getClass();
+ if (defaultsModule.getClass() != moduleClass) {
+ throw new IllegalArgumentException("Configuration modules must have the same type");
+ }
+ for (Field field : moduleClass.getDeclaredFields()) {
+ if (field.getAnnotation(DoNotLoad.class) != null
+ || field.getAnnotation(ConfigInfo.class) == null) {
+ continue;
+ }
+
+ validateField(moduleClass, field, false);
+ field.setAccessible(true);
+ field.set(module, copyValue(field.get(defaultsModule)));
+ }
+ }
+
+ // TODO[To-GitHub-issue]: Not sure whether needs to validate, we don't expose LeafConfig as public framework
+ private static void validateField(Class> moduleClass, Field field, boolean global) {
+ int modifiers = field.getModifiers();
+ if (Modifier.isFinal(modifiers)) {
+ throw new IllegalStateException("@ConfigInfo field must be mutable: "
+ + moduleClass.getName() + "." + field.getName());
+ }
+ if (Modifier.isStatic(modifiers) != global) {
+ String expected = global ? "static" : "an instance field";
+ throw new IllegalStateException("@ConfigInfo field must be " + expected + ": "
+ + moduleClass.getName() + "." + field.getName());
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static Object readValue(
+ LeafConfigAccessor config,
+ String path,
+ @Nullable String comment,
+ Field field,
+ Object defaultValue,
+ boolean writeDefault
+ ) {
+ Class> type = field.getType();
+ if (type == boolean.class || type == Boolean.class) {
+ if (!writeDefault) {
+ return config.configFile.getBoolean(path, (Boolean) defaultValue);
+ }
+ return comment == null
+ ? config.getBoolean(path, (Boolean) defaultValue)
+ : config.getBoolean(path, (Boolean) defaultValue, comment);
+ }
+ if (type == int.class || type == Integer.class) {
+ if (!writeDefault) {
+ return config.configFile.getInteger(path, (Integer) defaultValue);
+ }
+ return comment == null
+ ? config.getInt(path, (Integer) defaultValue)
+ : config.getInt(path, (Integer) defaultValue, comment);
+ }
+ if (type == long.class || type == Long.class) {
+ if (!writeDefault) {
+ return config.configFile.getLong(path, (Long) defaultValue);
+ }
+ return comment == null
+ ? config.getLong(path, (Long) defaultValue)
+ : config.getLong(path, (Long) defaultValue, comment);
+ }
+ if (type == double.class || type == Double.class) {
+ if (!writeDefault) {
+ return config.configFile.getDouble(path, (Double) defaultValue);
+ }
+ return comment == null
+ ? config.getDouble(path, (Double) defaultValue)
+ : config.getDouble(path, (Double) defaultValue, comment);
+ }
+ if (type == String.class) {
+ if (!writeDefault) {
+ return config.configFile.getString(path, (String) defaultValue);
+ }
+ return comment == null
+ ? config.getString(path, (String) defaultValue)
+ : config.getString(path, (String) defaultValue, comment);
+ }
+ if (List.class.isAssignableFrom(type)) {
+ if (!writeDefault) {
+ return config.configFile.getStringList(path);
+ }
+ return comment == null
+ ? config.getList(path, (List) defaultValue)
+ : config.getList(path, (List) defaultValue, comment);
+ }
+ if (type.isEnum()) {
+ String value;
+ if (!writeDefault) {
+ value = config.configFile.getString(path, ((Enum>) defaultValue).name());
+ } else {
+ value = comment == null
+ ? config.getString(path, ((Enum>) defaultValue).name())
+ : config.getString(path, ((Enum>) defaultValue).name(), comment);
+ }
+ try {
+ return Enum.valueOf((Class extends Enum>) type, value.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException exception) {
+ throw new IllegalArgumentException("Invalid value '" + value + "' for " + path
+ + "; expected one of " + List.of(type.getEnumConstants()), exception);
+ }
+ }
+ throw new IllegalArgumentException("Unsupported @ConfigInfo field type: " + field);
+ }
+
+ private static Object copyValue(Object value) {
+ if (value instanceof List> list) {
+ return new ArrayList<>(list);
+ }
+ return value;
+ }
+
+ private static Object globalDefaultValue(Field field) {
+ Object defaultValue = GLOBAL_DEFAULT_VALUES.get(field);
+ if (defaultValue == null) {
+ throw new IllegalStateException("Code default was not registered for configuration field: " + field);
+ }
+ return copyValue(defaultValue);
+ }
+
+ static final class PendingValue {
+
+ private final @Nullable Object target;
+ private final Field field;
+ private final Object value;
+ private final Object previousValue;
+
+ private PendingValue(@Nullable Object target, Field field, Object value) throws IllegalAccessException {
+ this.target = target;
+ this.field = field;
+ this.value = copyValue(value);
+ this.previousValue = copyValue(field.get(target));
+ }
+
+ void apply() throws IllegalAccessException {
+ this.field.set(this.target, copyValue(this.value));
+ }
+
+ void restore() throws IllegalAccessException {
+ this.field.set(this.target, copyValue(this.previousValue));
+ }
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigCategory.java b/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigCategory.java
index 8465456737..ec43d9d43f 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigCategory.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigCategory.java
@@ -1,5 +1,8 @@
package org.dreeam.leaf.config;
+/**
+ * Categories available to annotation-driven configuration modules.
+ */
public enum ConfigCategory {
ASYNC("async"),
PERF("performance"),
@@ -10,8 +13,6 @@ public enum ConfigCategory {
private final String basePath;
- private static final ConfigCategory[] VALUES = ConfigCategory.values();
-
ConfigCategory(String basePath) {
this.basePath = basePath;
}
@@ -19,8 +20,4 @@ public enum ConfigCategory {
public String basePath() {
return this.basePath;
}
-
- public static ConfigCategory[] categoryValues() {
- return VALUES;
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigModule.java b/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigModule.java
index c3ac3532eb..0e390b41f5 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigModule.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/ConfigModule.java
@@ -1,94 +1,24 @@
package org.dreeam.leaf.config;
-import it.unimi.dsi.fastutil.objects.ObjectArrays;
-import org.dreeam.leaf.config.annotations.Experimental;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Modifier;
-import java.util.*;
-
-public abstract class ConfigModule extends LeafConfig {
-
- private static final Set LOADED_MODULES = new HashSet<>();
-
- protected final LeafGlobalConfig globalConfig;
-
- public ConfigModule() {
- this.globalConfig = LeafConfig.globalConfig();
- }
-
- public static void initModules() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
- List enabledExperimentalModules = new ArrayList<>();
- List deprecatedModules = new ArrayList<>();
-
- Class>[] classes = LeafConfig.getClasses(LeafConfig.CONFIG_MODULE_PACKAGE).toArray(new Class[0]);
- ObjectArrays.quickSort(classes, Comparator.comparing(Class::getSimpleName));
- for (Class> clazz : classes) {
- ConfigModule module = (ConfigModule) clazz.getConstructor().newInstance();
- module.onLoaded();
-
- LOADED_MODULES.add(module);
- for (Field field : getAnnotatedStaticFields(clazz, Experimental.class)) {
- if (!(field.get(null) instanceof Boolean enabled)) continue;
- if (enabled) {
- enabledExperimentalModules.add(field);
- }
- }
- for (Field field : getAnnotatedStaticFields(clazz, Deprecated.class)) {
- if (!(field.get(null) instanceof Boolean enabled)) continue;
- if (enabled) {
- deprecatedModules.add(field);
- }
- }
- }
-
- if (!enabledExperimentalModules.isEmpty()) {
- LeafConfig.LOGGER.warn("You have following experimental module(s) enabled: {}, please proceed with caution!", formatModules(enabledExperimentalModules));
- }
-
- if (!deprecatedModules.isEmpty()) {
- LeafConfig.LOGGER.warn("The following enabled module(s) has been deprecated: {}, please proceed with caution!", formatModules(deprecatedModules));
- }
+/**
+ * Marker and lifecycle contract for a server-wide Leaf configuration module.
+ *
+ * Annotated fields must be static and mutable.
+ */
+public interface ConfigModule {
+
+ /**
+ * Runs after this module's configuration fields have been loaded.
+ *
+ * This hook runs during both initial configuration loading and reload. Core registries
+ * are not guaranteed to be available during the initial invocation.
+ */
+ default void onLoaded() {
}
- private static List formatModules(List modules) {
- return modules.stream().map(f -> f.getDeclaringClass().getSimpleName() + "." + f.getName()).toList();
- }
-
- public static void loadAfterBootstrap() {
- for (ConfigModule module : LOADED_MODULES) {
- module.onPostLoaded();
- }
-
- // Save config to disk
- try {
- LeafConfig.globalConfig().saveConfig();
- } catch (Exception e) {
- LeafConfig.LOGGER.error("Failed to save config file!", e);
- }
- }
-
- private static List getAnnotatedStaticFields(Class> clazz, Class extends Annotation> annotation) {
- List fields = new ArrayList<>();
-
- for (Field field : clazz.getDeclaredFields()) {
- if (field.isAnnotationPresent(annotation) && Modifier.isStatic(field.getModifiers())) {
- field.setAccessible(true);
- fields.add(field);
- }
- }
-
- return fields;
- }
-
- public static void clearModules() {
- LOADED_MODULES.clear();
- }
-
- public abstract void onLoaded();
-
- public void onPostLoaded() {
+ /**
+ * Runs after this module's configuration fields have been loaded and core registries are available.
+ */
+ default void onRegistriesLoaded() {
}
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfig.java b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfig.java
index f4e9cc0086..29c7c05413 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfig.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfig.java
@@ -1,12 +1,20 @@
package org.dreeam.leaf.config;
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
import io.papermc.paper.SparksFly;
+import it.unimi.dsi.fastutil.objects.ObjectArrays;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
-import net.minecraft.util.Util;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.dreeam.leaf.config.annotations.Experimental;
+import org.dreeam.leaf.config.annotations.HotReloadUnsupported;
+import org.dreeam.leaf.config.migration.LeafConfigMigration;
+import org.dreeam.leaf.config.migration.gale.GaleConfigMigration;
import org.dreeam.leaf.config.modules.misc.SentryDSN;
+import org.dreeam.leaf.config.util.ConfigFileIO;
import org.jspecify.annotations.NullMarked;
import org.bukkit.Bukkit;
import org.bukkit.World;
@@ -15,6 +23,8 @@
import java.io.File;
import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
@@ -26,6 +36,7 @@
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedHashSet;
@@ -45,37 +56,63 @@ public class LeafConfig {
public static final Logger LOGGER = LogManager.getLogger(LeafConfig.class.getSimpleName());
- protected static final String CURRENT_CONFIG_VERSION = "3.0";
+ public static final String CURRENT_CONFIG_VERSION = "3.0";
// It will be in uppercase by default, just make sure
private static final String REGION_COUNTRY_CODE = Locale.getDefault().getCountry().toUpperCase(Locale.ROOT);
private static final boolean IS_CHINESE_LOCALE = REGION_COUNTRY_CODE.equals("CN");
- protected static final File CONFIG_DIRECTORY = new File("config");
+ public static final File CONFIG_DIRECTORY = new File("config");
protected static final String CONFIG_MODULE_PACKAGE = "org.dreeam.leaf.config.modules";
protected static final String GLOBAL_CONFIG_FILE = "leaf-global.yml";
- protected static final String DEFAULT_WORLD_CONFIG_FILE = "leaf-world-defaults.yml"; // Leaf TODO - Per world config
+ protected static final String DEFAULT_WORLD_CONFIG_FILE = "leaf-world-defaults.yml";
+ protected static final String WORLD_CONFIG_FILE = "leaf-world.yml";
private static final String SPARK_EXTRA_CONFIG_PROPERTY = "spark.serverconfigs.extra";
private static final String SPARK_HIDDEN_PATHS_PROPERTY = "spark.serverconfigs.hiddenpaths";
private static LeafGlobalConfig globalConfig;
+ private static LeafWorldConfig worldDefaultsConfig;
- //private static int preMajorVer;
- private static int preMinorVer;
- //private static int currMajorVer;
- private static int currMinorVer;
+ private static final List GLOBAL_MODULES = new ArrayList<>();
+ private static final List WORLD_MODULES = new ArrayList<>();
+
+ private static ConfigVersion previousConfigVersion = ConfigVersion.initial();
/* Load & Reload */
- // Reload config (async)
+ // Reload config on the server thread
public static CompletableFuture reloadAsync(CommandSender sender) {
+ MinecraftServer server = MinecraftServer.getServer();
return CompletableFuture.runAsync(() -> {
try {
long begin = System.nanoTime();
- ConfigModule.clearModules();
- loadConfig(false);
- ConfigModule.loadAfterBootstrap();
+ createDirectory(CONFIG_DIRECTORY);
+
+ ConfigFile globalConfigFile = ConfigFileIO.load(new File(CONFIG_DIRECTORY, GLOBAL_CONFIG_FILE));
+ ConfigFile worldDefaultsFile = ConfigFileIO.load(new File(CONFIG_DIRECTORY, DEFAULT_WORLD_CONFIG_FILE));
+ LeafGlobalConfig loadedGlobalConfig = new LeafGlobalConfig(globalConfigFile, false);
+ LeafWorldConfig loadedWorldDefaults = loadWorldDefaults(worldDefaultsFile);
+
+ List pendingValues = new ArrayList<>();
+ if (GLOBAL_MODULES.isEmpty()) {
+ discoverGlobalModules();
+ }
+ for (ConfigModule module : GLOBAL_MODULES) {
+ ConfigBinder.collectGlobalReload(module, loadedGlobalConfig, pendingValues);
+ }
+ collectWorldReloadValues(worldDefaultsConfig, loadedWorldDefaults, pendingValues);
+
+ List worldReloads = new ArrayList<>();
+ for (ServerLevel level : server.getAllLevels()) {
+ Path worldDirectory = server.storageSource.getDimensionPath(level.dimension());
+ LeafWorldConfig loadedConfig = loadWorldConfig(worldDirectory, loadedWorldDefaults);
+ LeafWorldConfig currentConfig = level.leafConfig();
+ collectWorldReloadValues(currentConfig, loadedConfig, pendingValues);
+ worldReloads.add(new WorldReload(currentConfig, loadedConfig.configFile, currentConfig.configFile));
+ }
+
+ commitReload(loadedGlobalConfig, loadedWorldDefaults, worldReloads, pendingValues);
final String success = String.format("Successfully reloaded config in %sms.", (System.nanoTime() - begin) / 1_000_000);
Command.broadcastCommandMessage(sender, Component.text(success, NamedTextColor.GREEN));
@@ -83,7 +120,27 @@ public static CompletableFuture reloadAsync(CommandSender sender) {
Command.broadcastCommandMessage(sender, Component.text("Failed to reload config. See error in console!", NamedTextColor.RED));
LOGGER.error("Failed to reload config!", e);
}
- }, Util.ioPool());
+ }, server);
+ }
+
+ private static LeafWorldConfig loadWorldConfig(Path worldDirectory, LeafWorldConfig defaults) throws Exception {
+ LeafWorldConfig config = new LeafWorldConfig(
+ defaults.configFile,
+ LeafWorldConfig.Source.WORLD_OVERRIDE
+ );
+ applyWorldDefaults(config, defaults);
+
+ File worldConfigFile = worldDirectory.resolve(WORLD_CONFIG_FILE).toFile();
+ if (!worldConfigFile.isFile()) {
+ config.setConfigFile(defaults.configFile);
+ return config;
+ }
+ applyWorldOverride(
+ config,
+ ConfigFileIO.load(worldConfigFile),
+ defaults
+ );
+ return config;
}
// Init config
@@ -93,7 +150,30 @@ public static void loadConfig() {
LOGGER.info("Loading config...");
purgeOutdated();
- loadConfig(true);
+ createDirectory(CONFIG_DIRECTORY);
+
+ ConfigFile globalConfigFile = ConfigFileIO.load(new File(CONFIG_DIRECTORY, GLOBAL_CONFIG_FILE));
+ ConfigFile worldDefaultsFile = ConfigFileIO.load(new File(CONFIG_DIRECTORY, DEFAULT_WORLD_CONFIG_FILE));
+
+ // Migrate the same raw config instances that will be bound and saved below.
+ LeafConfigMigration.migrate(globalConfigFile, worldDefaultsFile);
+ GaleConfigMigration.migrate(
+ CONFIG_DIRECTORY.toPath(),
+ globalConfigFile,
+ worldDefaultsFile
+ );
+
+ globalConfig = new LeafGlobalConfig(globalConfigFile, true);
+ if (GLOBAL_MODULES.isEmpty()) {
+ discoverGlobalModules();
+ }
+ for (ConfigModule module : GLOBAL_MODULES) {
+ ConfigBinder.bind(module, null, globalConfig, true);
+ }
+ runGlobalModuleCallbacks(false);
+
+ worldDefaultsConfig = loadWorldDefaults(worldDefaultsFile);
+ worldDefaultsConfig.saveConfig();
LOGGER.info("Successfully loaded config in {}ms.", (System.nanoTime() - begin) / 1_000_000);
} catch (Exception e) {
@@ -103,18 +183,239 @@ public static void loadConfig() {
/* Load Global Config */
- private static void loadConfig(boolean init) throws Exception {
- // Create config folder
- createDirectory(CONFIG_DIRECTORY);
+ public static LeafGlobalConfig globalConfig() {
+ return globalConfig;
+ }
+
+ public static LeafWorldConfig worldDefaultsConfig() {
+ return worldDefaultsConfig;
+ }
- globalConfig = new LeafGlobalConfig(init);
+ /**
+ * Creates a world configuration from the shared defaults and applies an existing override
+ * without creating a file when no override exists.
+ */
+ public static LeafWorldConfig initWorldConfig(Path worldDirectory) {
+ File worldConfigFile = worldDirectory.resolve(WORLD_CONFIG_FILE).toFile();
+ LeafWorldConfig migratedConfig = GaleConfigMigration.migrateWorldOverride(
+ worldDirectory,
+ worldConfigFile,
+ worldDefaultsConfig
+ );
+ if (migratedConfig != null) {
+ return migratedConfig;
+ }
+ try {
+ return loadWorldConfig(worldDirectory, worldDefaultsConfig);
+ } catch (Exception exception) {
+ throw new RuntimeException("Could not load Leaf world config for " + worldDirectory, exception);
+ }
+ }
- // Load config modules
- ConfigModule.initModules();
+ private static void discoverGlobalModules() throws ReflectiveOperationException {
+ Class>[] classes = getClasses(CONFIG_MODULE_PACKAGE).toArray(new Class[0]);
+ ObjectArrays.quickSort(classes, Comparator.comparing((Class> clazz) -> clazz.getSimpleName())
+ .thenComparing(Class::getName));
+ for (Class> moduleClass : classes) {
+ if (moduleClass.isInterface() || Modifier.isAbstract(moduleClass.getModifiers())) {
+ continue;
+ }
+
+ if (!ConfigModule.class.isAssignableFrom(moduleClass)) {
+ continue;
+ }
+
+ ConfigModule module = (ConfigModule) moduleClass.getConstructor().newInstance();
+ ConfigBinder.registerGlobalDefaults(module);
+ GLOBAL_MODULES.add(module);
+ }
}
- public static LeafGlobalConfig globalConfig() {
- return globalConfig;
+ private static void runGlobalModuleCallbacks(boolean reload) throws IllegalAccessException {
+ List enabledExperimentalModules = new ArrayList<>();
+ List deprecatedModules = new ArrayList<>();
+
+ for (ConfigModule module : GLOBAL_MODULES) {
+ Class> moduleClass = module.getClass();
+ if (reload && moduleClass.isAnnotationPresent(HotReloadUnsupported.class)) {
+ continue;
+ }
+
+ module.onLoaded();
+
+ for (Field field : moduleClass.getDeclaredFields()) {
+ boolean experimental = field.isAnnotationPresent(Experimental.class);
+ boolean deprecated = field.isAnnotationPresent(Deprecated.class);
+ if ((!experimental && !deprecated) || !Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+ field.setAccessible(true);
+ if (field.get(null) instanceof Boolean enabled && enabled) {
+ if (experimental) {
+ enabledExperimentalModules.add(field);
+ }
+ if (deprecated) {
+ deprecatedModules.add(field);
+ }
+ }
+ }
+ }
+
+ warnEnabledModules(
+ enabledExperimentalModules,
+ "You have following experimental module(s) enabled: {}, please proceed with caution!"
+ );
+ warnEnabledModules(
+ deprecatedModules,
+ "The following enabled module(s) has been deprecated: {}, please proceed with caution!"
+ );
+ }
+
+ private static void collectWorldReloadValues(
+ LeafWorldConfig config,
+ LeafWorldConfig loadedConfig,
+ List pendingValues
+ ) throws IllegalAccessException {
+ for (Field moduleField : WORLD_MODULES) {
+ WorldConfigModule module = (WorldConfigModule) moduleField.get(config);
+ WorldConfigModule loadedModule = (WorldConfigModule) moduleField.get(loadedConfig);
+ ConfigBinder.collectWorldReload(module, loadedModule, pendingValues);
+ }
+ }
+
+ private static void commitReload(
+ LeafGlobalConfig loadedGlobalConfig,
+ LeafWorldConfig loadedWorldDefaults,
+ List worldReloads,
+ List pendingValues
+ ) throws Exception {
+ LeafGlobalConfig previousGlobalConfig = globalConfig;
+ ConfigFile previousWorldDefaultsFile = worldDefaultsConfig.configFile;
+ int appliedValues = 0;
+
+ try {
+ globalConfig = loadedGlobalConfig;
+ worldDefaultsConfig.setConfigFile(loadedWorldDefaults.configFile);
+ for (WorldReload worldReload : worldReloads) {
+ worldReload.config().setConfigFile(worldReload.loadedConfigFile());
+ }
+ for (ConfigBinder.PendingValue pendingValue : pendingValues) {
+ pendingValue.apply();
+ appliedValues++;
+ }
+
+ runGlobalModuleCallbacks(true);
+ runAfterBootstrapCallbacks(true);
+ ConfigFileIO.saveAtomically(worldDefaultsConfig.configFile, globalConfig.configFile);
+ } catch (Exception exception) {
+ for (int index = appliedValues - 1; index >= 0; index--) {
+ try {
+ pendingValues.get(index).restore();
+ } catch (IllegalAccessException restoreException) {
+ exception.addSuppressed(restoreException);
+ }
+ }
+ globalConfig = previousGlobalConfig;
+ worldDefaultsConfig.setConfigFile(previousWorldDefaultsFile);
+ for (WorldReload worldReload : worldReloads) {
+ worldReload.config().setConfigFile(worldReload.previousConfigFile());
+ }
+
+ try {
+ runGlobalModuleCallbacks(true);
+ } catch (Exception callbackException) {
+ exception.addSuppressed(callbackException);
+ }
+ try {
+ runAfterBootstrapCallbacks(true);
+ } catch (Exception callbackException) {
+ exception.addSuppressed(callbackException);
+ }
+ throw exception;
+ }
+ }
+
+ private static LeafWorldConfig loadWorldDefaults(ConfigFile configFile) throws ReflectiveOperationException {
+ if (WORLD_MODULES.isEmpty()) {
+ Field[] fields = LeafWorldConfig.class.getDeclaredFields();
+ ObjectArrays.quickSort(fields, Comparator.comparing((Field field) -> field.getType().getSimpleName())
+ .thenComparing(field -> field.getType().getName()));
+ for (Field field : fields) {
+ if (WorldConfigModule.class.isAssignableFrom(field.getType())) {
+ WORLD_MODULES.add(field);
+ }
+ }
+ }
+
+ LeafWorldConfig config = new LeafWorldConfig(configFile, LeafWorldConfig.Source.WORLD_DEFAULTS);
+ for (Field moduleField : WORLD_MODULES) {
+ WorldConfigModule module = (WorldConfigModule) moduleField.get(config);
+ ConfigBinder.bind(module, null, config, false);
+ }
+ return config;
+ }
+
+ public static LeafWorldConfig loadWorldOverride(
+ ConfigFile configFile,
+ LeafWorldConfig worldDefaults
+ ) throws ReflectiveOperationException {
+ LeafWorldConfig config = new LeafWorldConfig(worldDefaults.configFile, LeafWorldConfig.Source.WORLD_OVERRIDE);
+ applyWorldDefaults(config, worldDefaults);
+ applyWorldOverride(config, configFile, worldDefaults);
+ return config;
+ }
+
+ private static void applyWorldOverride(
+ LeafWorldConfig config,
+ ConfigFile configFile,
+ LeafWorldConfig worldDefaults
+ ) throws ReflectiveOperationException {
+ config.setConfigFile(configFile);
+
+ for (Field moduleField : WORLD_MODULES) {
+ WorldConfigModule module = (WorldConfigModule) moduleField.get(config);
+ WorldConfigModule worldDefaultModule = (WorldConfigModule) moduleField.get(worldDefaults);
+ ConfigBinder.bind(module, worldDefaultModule, config, false);
+ }
+ }
+
+ public static void loadAfterBootstrap() {
+ runAfterBootstrapCallbacks(false);
+
+ try {
+ globalConfig.saveConfig();
+ } catch (Exception exception) {
+ LOGGER.error("Failed to save config file!", exception);
+ }
+ }
+
+ private static void runAfterBootstrapCallbacks(boolean reload) {
+ for (ConfigModule module : GLOBAL_MODULES) {
+ if (reload && module.getClass().isAnnotationPresent(HotReloadUnsupported.class)) {
+ continue;
+ }
+ module.onRegistriesLoaded();
+ }
+ }
+
+ private static void warnEnabledModules(List fields, String message) {
+ if (fields.isEmpty()) {
+ return;
+ }
+ LOGGER.warn(message, fields.stream()
+ .map(field -> field.getDeclaringClass().getSimpleName() + "." + field.getName())
+ .toList());
+ }
+
+ private static void applyWorldDefaults(
+ LeafWorldConfig config,
+ LeafWorldConfig defaults
+ ) throws IllegalAccessException {
+ for (Field moduleField : WORLD_MODULES) {
+ WorldConfigModule module = (WorldConfigModule) moduleField.get(config);
+ WorldConfigModule defaultsModule = (WorldConfigModule) moduleField.get(defaults);
+ ConfigBinder.applyWorldDefaults(module, defaultsModule);
+ }
}
static boolean isChineseLocale() {
@@ -216,14 +517,107 @@ private static void findClassesInPackageByJar(String packageName, EnumerationUse this when migrating a renamed option path, before the current config version is
+ * persisted. For example, {@code isConfigVersionBefore("3.1")} is {@code true} for a
+ * configuration last written by Leaf 3.0.
+ *
+ * @param version a numeric dot-separated config version, such as {@code 3.1}
+ * @return {@code true} when the loaded configuration is older than {@code version}
+ * @throws IllegalArgumentException if {@code version} is not a numeric dot-separated version
+ */
+ public static boolean isConfigVersionBefore(String version) {
+ return previousConfigVersion.compareTo(ConfigVersion.parse(version)) < 0;
+ }
+
+ static boolean isConfigVersionBefore(String storedVersion, String version) {
+ return parseStoredConfigVersion(storedVersion, false).compareTo(ConfigVersion.parse(version)) < 0;
+ }
+
+ /**
+ * Returns whether the configuration being loaded is at least {@code version}.
+ *
+ * @param version a numeric dot-separated config version, such as {@code 3.1}
+ * @return {@code true} when the loaded configuration is not older than {@code version}
+ * @throws IllegalArgumentException if {@code version} is not a numeric dot-separated version
+ */
+ public static boolean isConfigVersionAtLeast(String version) {
+ return !isConfigVersionBefore(version);
+ }
+
+ private static ConfigVersion parseStoredConfigVersion(String version, boolean warnIfInvalid) {
+ if (version == null) {
+ return ConfigVersion.initial();
+ }
+
+ try {
+ return ConfigVersion.parse(version);
+ } catch (IllegalArgumentException exception) {
+ if (warnIfInvalid) {
+ LOGGER.warn("Invalid Leaf config version '{}'; treating it as an unversioned configuration.", version);
+ }
+ return ConfigVersion.initial();
+ }
+ }
+
+ private record WorldReload(
+ LeafWorldConfig config,
+ ConfigFile loadedConfigFile,
+ ConfigFile previousConfigFile
+ ) {
+ }
- // First time user
- if (preVer == null) {
+ private record ConfigVersion(List components) implements Comparable {
+ private static ConfigVersion initial() {
+ return new ConfigVersion(List.of(0));
+ }
+
+ private static ConfigVersion parse(String version) {
+ if (version == null || version.isBlank()) {
+ throw new IllegalArgumentException("Config version must not be blank");
+ }
+
+ String[] parts = version.split("\\.", -1);
+ List components = new ArrayList<>(parts.length);
+ for (String part : parts) {
+ if (part.isEmpty() || !part.chars().allMatch(Character::isDigit)) {
+ throw new IllegalArgumentException("Invalid config version: " + version);
+ }
+ try {
+ components.add(Integer.parseInt(part));
+ } catch (NumberFormatException exception) {
+ throw new IllegalArgumentException("Invalid config version: " + version, exception);
+ }
+ }
+ return new ConfigVersion(List.copyOf(components));
+ }
+
+ @Override
+ public int compareTo(ConfigVersion other) {
+ int componentCount = Math.max(this.components.size(), other.components.size());
+ for (int index = 0; index < componentCount; index++) {
+ int thisComponent = index < this.components.size() ? this.components.get(index) : 0;
+ int otherComponent = index < other.components.size() ? other.components.get(index) : 0;
+ int comparison = Integer.compare(thisComponent, otherComponent);
+ if (comparison != 0) {
+ return comparison;
+ }
+ }
+ return 0;
}
}
@@ -232,8 +626,7 @@ public static void loadConfigVersion(String preVer, String currVer) {
private static List buildSparkExtraConfigs() {
List extraConfigs = new ArrayList<>(Arrays.asList(
"config/leaf-global.yml",
- "config/gale-global.yml",
- "config/gale-world-defaults.yml"
+ "config/leaf-world-defaults.yml"
));
String existing = System.getProperty(SPARK_EXTRA_CONFIG_PROPERTY);
@@ -246,17 +639,21 @@ private static List buildSparkExtraConfigs() {
// instead of using SplitYamlConfigParser.INSTANCE for the extra config
// However it's better to choose bundled spark for better view.
for (World world : Bukkit.getWorlds()) {
- Path galeWorldFolder = world.getWorldFolder().toPath().resolve("gale-world.yml");
- extraConfigs.add(galeWorldFolder.toString().replace("\\", "/").replace("./", "")); // Gale world config
+ Path leafWorldFile = world.getWorldFolder().toPath().resolve(WORLD_CONFIG_FILE);
+ extraConfigs.add(leafWorldFile.toString().replace("\\", "/").replace("./", "")); // Leaf world override config
}
return extraConfigs;
}
private static List buildSparkHiddenPaths() {
+ List extraHidden = new ArrayList<>();
+
String existing = System.getProperty(SPARK_HIDDEN_PATHS_PROPERTY);
+ if (existing != null) {
+ extraHidden.addAll(Arrays.asList(existing.split(",")));
+ }
- List extraHidden = existing != null ? new ArrayList<>(Arrays.asList(existing.split(","))) : new ArrayList<>();
extraHidden.add(SentryDSN.sentryDsnConfigPath); // Hide Sentry DSN key
return extraHidden;
@@ -282,9 +679,8 @@ private static void purgeOutdated() {
String leafConfigV1 = "leaf.yml";
String leafConfigV2 = "leaf_config";
- Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddhhmmss");
- String backupDir = "config/backup" + dateFormat.format(date) + "/";
+ String backupDir = "config/backup" + dateFormat.format(new Date()) + "/";
File pufferfishConfigFile = new File(pufferfishConfig);
File leafConfigV1File = new File(leafConfigV1);
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfigAccessor.java b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfigAccessor.java
new file mode 100644
index 0000000000..9bd387fc26
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfigAccessor.java
@@ -0,0 +1,191 @@
+package org.dreeam.leaf.config;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
+import org.dreeam.leaf.config.migration.ConfigPathMigration;
+import org.dreeam.leaf.config.util.ConfigFileIO;
+import org.jspecify.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+/** Shared configuration-file utilities for global and world configuration views. */
+abstract class LeafConfigAccessor {
+
+ protected ConfigFile configFile;
+
+ protected LeafConfigAccessor(ConfigFile configFile) {
+ this.configFile = configFile;
+ }
+
+ void setConfigFile(ConfigFile configFile) {
+ this.configFile = configFile;
+ }
+
+ public void saveConfig() throws Exception {
+ ConfigFileIO.saveAtomically(configFile);
+ }
+
+ boolean contains(String path) {
+ return configFile.contains(path);
+ }
+
+ public boolean migratePath(String oldPath, String newPath) {
+ return ConfigPathMigration.migrate(configFile, oldPath, newPath);
+ }
+
+ public void createTitledSection(String title, String path) {
+ configFile.addSection(title);
+ configFile.addDefault(path, null);
+ }
+
+ public boolean getBoolean(String path, boolean def, String comment) {
+ configFile.addDefault(path, def, comment);
+ return configFile.getBoolean(path, def);
+ }
+
+ public boolean getBoolean(String path, boolean def) {
+ configFile.addDefault(path, def);
+ return configFile.getBoolean(path, def);
+ }
+
+ public String getString(String path, String def, String comment) {
+ configFile.addDefault(path, def, comment);
+ return configFile.getString(path, def);
+ }
+
+ public String getString(String path, String def) {
+ configFile.addDefault(path, def);
+ return configFile.getString(path, def);
+ }
+
+ public double getDouble(String path, double def, String comment) {
+ configFile.addDefault(path, def, comment);
+ return configFile.getDouble(path, def);
+ }
+
+ public double getDouble(String path, double def) {
+ configFile.addDefault(path, def);
+ return configFile.getDouble(path, def);
+ }
+
+ public int getInt(String path, int def, String comment) {
+ configFile.addDefault(path, def, comment);
+ return configFile.getInteger(path, def);
+ }
+
+ public int getInt(String path, int def) {
+ configFile.addDefault(path, def);
+ return configFile.getInteger(path, def);
+ }
+
+ public long getLong(String path, long def, String comment) {
+ configFile.addDefault(path, def, comment);
+ return configFile.getLong(path, def);
+ }
+
+ public long getLong(String path, long def) {
+ configFile.addDefault(path, def);
+ return configFile.getLong(path, def);
+ }
+
+ public List getList(String path, List def, String comment) {
+ configFile.addDefault(path, def, comment);
+ return configFile.getStringList(path);
+ }
+
+ public List getList(String path, List def) {
+ configFile.addDefault(path, def);
+ return configFile.getStringList(path);
+ }
+
+ public ConfigSection getConfigSection(String path, Map defaultKeyValue, String comment) {
+ configFile.addDefault(path, null, comment);
+ configFile.makeSectionLenient(path);
+ defaultKeyValue.forEach((key, value) -> configFile.addExample(path + "." + key, value));
+ return configFile.getConfigSection(path);
+ }
+
+ public ConfigSection getConfigSection(String path, Map defaultKeyValue) {
+ configFile.addDefault(path, null);
+ configFile.makeSectionLenient(path);
+ defaultKeyValue.forEach((key, value) -> configFile.addExample(path + "." + key, value));
+ return configFile.getConfigSection(path);
+ }
+
+ public Boolean getBoolean(String path) {
+ String value = configFile.getString(path, null);
+ return value == null ? null : Boolean.parseBoolean(value);
+ }
+
+ public String getString(String path) {
+ return configFile.getString(path, null);
+ }
+
+ public Double getDouble(String path) {
+ String value = configFile.getString(path, null);
+ if (value == null) return null;
+ try {
+ return Double.parseDouble(value);
+ } catch (NumberFormatException exception) {
+ LeafConfig.LOGGER.warn("{} is not a valid number, skipped! Please check your configuration.", path, exception);
+ return null;
+ }
+ }
+
+ public Integer getInt(String path) {
+ String value = configFile.getString(path, null);
+ if (value == null) return null;
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException exception) {
+ LeafConfig.LOGGER.warn("{} is not a valid number, skipped! Please check your configuration.", path, exception);
+ return null;
+ }
+ }
+
+ public Long getLong(String path) {
+ String value = configFile.getString(path, null);
+ if (value == null) return null;
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException exception) {
+ LeafConfig.LOGGER.warn("{} is not a valid number, skipped! Please check your configuration.", path, exception);
+ return null;
+ }
+ }
+
+ public List getList(String path) {
+ return configFile.getList(path, null);
+ }
+
+ public ConfigSection getConfigSection(String path) {
+ configFile.addDefault(path, null);
+ configFile.makeSectionLenient(path);
+ return configFile.getConfigSection(path);
+ }
+
+ public void addComment(String path, String comment) {
+ configFile.addComment(path, comment);
+ }
+
+ public void addCommentIfCN(String path, String comment) {
+ if (LeafConfig.isChineseLocale()) configFile.addComment(path, comment);
+ }
+
+ public void addCommentIfNonCN(String path, String comment) {
+ if (!LeafConfig.isChineseLocale()) configFile.addComment(path, comment);
+ }
+
+ public void addCommentRegionBased(String path, String en, String cn) {
+ configFile.addComment(path, LeafConfig.isChineseLocale() ? cn : en);
+ }
+
+ // TODO - why so complicated?
+ public @Nullable String pickStringRegionBased(String... localizedStrings) {
+ if (localizedStrings == null || localizedStrings.length == 0) return null;
+ if (localizedStrings.length == 1) return localizedStrings[0];
+ return LeafConfig.isChineseLocale() ? localizedStrings[1] : localizedStrings[0];
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/LeafGlobalConfig.java b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafGlobalConfig.java
index 1984a855f7..67b91b29ba 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/LeafGlobalConfig.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafGlobalConfig.java
@@ -1,24 +1,22 @@
package org.dreeam.leaf.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
-import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
-import java.io.File;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
+import java.util.Objects;
-public class LeafGlobalConfig {
+/** The server-wide Leaf configuration. */
+public final class LeafGlobalConfig extends LeafConfigAccessor {
- private static ConfigFile configFile;
+ LeafGlobalConfig(ConfigFile configFile, boolean loadPreviousVersion) {
+ super(configFile);
- public LeafGlobalConfig(boolean init) throws Exception {
- configFile = ConfigFile.loadConfig(new File(LeafConfig.CONFIG_DIRECTORY, LeafConfig.GLOBAL_CONFIG_FILE));
+ if (loadPreviousVersion) {
+ LeafConfig.loadPreviousConfigVersion(getString("config-version"));
+ }
- //LeafConfig.loadPreviousConfigVersion(getString("config-version"));
configFile.set("config-version", LeafConfig.CURRENT_CONFIG_VERSION);
- configFile.addComments("config-version", pickStringRegionBased("""
+ configFile.addComments("config-version", Objects.requireNonNull(pickStringRegionBased("""
Leaf Config
Website: https://www.leafmc.one/
@@ -31,188 +29,14 @@ public LeafGlobalConfig(boolean init) throws Exception {
官网: https://www.leafmc.one/zh/
文档: https://www.leafmc.one/zh/docs/getting-started
GitHub 仓库: https://github.com/Winds-Studio/Leaf
- QQ社区群: 619278377"""));
+ QQ社区群: 619278377""")));
- // Pre-structure to force order
structureConfig();
}
- protected void structureConfig() {
- for (ConfigCategory category : ConfigCategory.categoryValues()) {
+ private void structureConfig() {
+ for (ConfigCategory category : ConfigCategory.values()) {
createTitledSection(category.name(), category.basePath());
}
}
-
- public void saveConfig() throws Exception {
- configFile.save();
- }
-
- // Config Utilities
-
- /* getAndSet */
-
- public void createTitledSection(String title, String path) {
- configFile.addSection(title);
- configFile.addDefault(path, null);
- }
-
- public boolean getBoolean(String path, boolean def, String comment) {
- configFile.addDefault(path, def, comment);
- return configFile.getBoolean(path, def);
- }
-
- public boolean getBoolean(String path, boolean def) {
- configFile.addDefault(path, def);
- return configFile.getBoolean(path, def);
- }
-
- public String getString(String path, String def, String comment) {
- configFile.addDefault(path, def, comment);
- return configFile.getString(path, def);
- }
-
- public String getString(String path, String def) {
- configFile.addDefault(path, def);
- return configFile.getString(path, def);
- }
-
- public double getDouble(String path, double def, String comment) {
- configFile.addDefault(path, def, comment);
- return configFile.getDouble(path, def);
- }
-
- public double getDouble(String path, double def) {
- configFile.addDefault(path, def);
- return configFile.getDouble(path, def);
- }
-
- public int getInt(String path, int def, String comment) {
- configFile.addDefault(path, def, comment);
- return configFile.getInteger(path, def);
- }
-
- public int getInt(String path, int def) {
- configFile.addDefault(path, def);
- return configFile.getInteger(path, def);
- }
-
- public long getLong(String path, long def, String comment) {
- configFile.addDefault(path, def, comment);
- return configFile.getLong(path, def);
- }
-
- public long getLong(String path, long def) {
- configFile.addDefault(path, def);
- return configFile.getLong(path, def);
- }
-
- public List getList(String path, List def, String comment) {
- configFile.addDefault(path, def, comment);
- return configFile.getStringList(path);
- }
-
- public List getList(String path, List def) {
- configFile.addDefault(path, def);
- return configFile.getStringList(path);
- }
-
- public ConfigSection getConfigSection(String path, Map defaultKeyValue, String comment) {
- configFile.addDefault(path, null, comment);
- configFile.makeSectionLenient(path);
- defaultKeyValue.forEach((string, object) -> configFile.addExample(path + "." + string, object));
- return configFile.getConfigSection(path);
- }
-
- public ConfigSection getConfigSection(String path, Map defaultKeyValue) {
- configFile.addDefault(path, null);
- configFile.makeSectionLenient(path);
- defaultKeyValue.forEach((string, object) -> configFile.addExample(path + "." + string, object));
- return configFile.getConfigSection(path);
- }
-
- /* get */
-
- public Boolean getBoolean(String path) {
- String value = configFile.getString(path, null);
- return value == null ? null : Boolean.parseBoolean(value);
- }
-
- public String getString(String path) {
- return configFile.getString(path, null);
- }
-
- public Double getDouble(String path) {
- String value = configFile.getString(path, null);
- if (value == null) {
- return null;
- }
- try {
- return Double.parseDouble(value);
- } catch (NumberFormatException e) {
- LeafConfig.LOGGER.warn("{} is not a valid number, skipped! Please check your configuration.", path, e);
- return null;
- }
- }
-
- public Integer getInt(String path) {
- String value = configFile.getString(path, null);
- if (value == null) {
- return null;
- }
- try {
- return Integer.parseInt(value);
- } catch (NumberFormatException e) {
- LeafConfig.LOGGER.warn("{} is not a valid number, skipped! Please check your configuration.", path, e);
- return null;
- }
- }
-
- public Long getLong(String path) {
- String value = configFile.getString(path, null);
- if (value == null) {
- return null;
- }
- try {
- return Long.parseLong(value);
- } catch (NumberFormatException e) {
- LeafConfig.LOGGER.warn("{} is not a valid number, skipped! Please check your configuration.", path, e);
- return null;
- }
- }
-
- public List getList(String path) {
- return configFile.getList(path, null);
- }
-
- // TODO, check
- public ConfigSection getConfigSection(String path) {
- configFile.addDefault(path, null);
- configFile.makeSectionLenient(path);
- //defaultKeyValue.forEach((string, object) -> configFile.addExample(path + "." + string, object));
- return configFile.getConfigSection(path);
- }
-
- public void addComment(String path, String comment) {
- configFile.addComment(path, comment);
- }
-
- public void addCommentIfCN(String path, String comment) {
- if (LeafConfig.isChineseLocale()) {
- configFile.addComment(path, comment);
- }
- }
-
- public void addCommentIfNonCN(String path, String comment) {
- if (!LeafConfig.isChineseLocale()) {
- configFile.addComment(path, comment);
- }
- }
-
- public void addCommentRegionBased(String path, String en, String cn) {
- configFile.addComment(path, LeafConfig.isChineseLocale() ? cn : en);
- }
-
- public String pickStringRegionBased(String en, String cn) {
- return LeafConfig.isChineseLocale() ? cn : en;
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/LeafWorldConfig.java b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafWorldConfig.java
new file mode 100644
index 0000000000..ff1d910acf
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/LeafWorldConfig.java
@@ -0,0 +1,56 @@
+package org.dreeam.leaf.config;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.dreeam.leaf.config.modules.fixes.world.Fixes;
+import org.dreeam.leaf.config.modules.gameplay.world.EnderDragonRespawn;
+import org.dreeam.leaf.config.modules.gameplay.world.HideFlamesOnEntitiesWithFireResistance;
+import org.dreeam.leaf.config.modules.gameplay.world.RandomStrollIntoNonTickingChunks;
+import org.dreeam.leaf.config.modules.misc.WorldConfigExample;
+import org.dreeam.leaf.config.modules.misc.world.SecureSeed;
+import org.dreeam.leaf.config.modules.opt.world.EntityWakeUpDuration;
+import org.dreeam.leaf.config.modules.opt.world.LoadChunks;
+import org.dreeam.leaf.config.modules.opt.world.MaxProjectileChunkLoads;
+import org.dreeam.leaf.config.modules.opt.world.OptimizedSheepOffspringColor;
+import org.dreeam.leaf.config.modules.opt.world.ReducedIntervals;
+import org.dreeam.leaf.config.modules.opt.world.SaveFireworks;
+
+/**
+ * A world-level configuration initialized from {@link LeafConfig#worldDefaultsConfig()} with an
+ * optional per-world overlay.
+ *
+ * The file is never created by this class. Every world receives its own configuration instance,
+ * initially inherits the shared defaults, and then applies values explicitly defined in
+ * {@code leaf-world.yml}. World modules are exposed as typed fields for direct access through a
+ * level's Leaf configuration.
+ */
+public final class LeafWorldConfig extends LeafConfigAccessor {
+
+ enum Source {
+ WORLD_DEFAULTS,
+ WORLD_OVERRIDE
+ }
+
+ private final Source source;
+
+ public WorldConfigExample worldConfigExample = new WorldConfigExample();
+ public SaveFireworks saveFireworks = new SaveFireworks();
+ public OptimizedSheepOffspringColor optimizedSheepOffspringColor = new OptimizedSheepOffspringColor();
+ public MaxProjectileChunkLoads maxProjectileChunkLoads = new MaxProjectileChunkLoads();
+ public ReducedIntervals reducedIntervals = new ReducedIntervals();
+ public LoadChunks loadChunks = new LoadChunks();
+ public Fixes fixes = new Fixes();
+ public RandomStrollIntoNonTickingChunks randomStrollIntoNonTickingChunks = new RandomStrollIntoNonTickingChunks();
+ public EntityWakeUpDuration entityWakeUpDuration = new EntityWakeUpDuration();
+ public HideFlamesOnEntitiesWithFireResistance hideFlamesOnEntitiesWithFireResistance = new HideFlamesOnEntitiesWithFireResistance();
+ public EnderDragonRespawn enderDragonRespawn = new EnderDragonRespawn();
+ public SecureSeed secureSeed = new SecureSeed();
+
+ LeafWorldConfig(ConfigFile configFile, Source source) {
+ super(configFile);
+ this.source = source;
+ }
+
+ public boolean isWorldDefaults() {
+ return this.source == Source.WORLD_DEFAULTS;
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/WorldConfigModule.java b/leaf-server/src/main/java/org/dreeam/leaf/config/WorldConfigModule.java
new file mode 100644
index 0000000000..377d52c439
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/WorldConfigModule.java
@@ -0,0 +1,10 @@
+package org.dreeam.leaf.config;
+
+/**
+ * Marker for a world-scoped Leaf configuration module.
+ *
+ * Annotated fields must be mutable instance fields. Implementations must be exposed as typed
+ * fields on {@link LeafWorldConfig}.
+ */
+public interface WorldConfigModule {
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/ConfigClassInfo.java b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/ConfigClassInfo.java
new file mode 100644
index 0000000000..c88165cb64
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/ConfigClassInfo.java
@@ -0,0 +1,26 @@
+package org.dreeam.leaf.config.annotations;
+
+import org.dreeam.leaf.config.ConfigCategory;
+
+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;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ConfigClassInfo {
+
+ ConfigCategory category();
+
+ String name() default "";
+
+ /**
+ * Optional section comment. Text blocks are supported.
+ *
+ * Supply one value for all locales, or English and Chinese values in that order.
+ */
+ String[] comments() default {};
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/ConfigInfo.java b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/ConfigInfo.java
new file mode 100644
index 0000000000..6d57a9d118
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/ConfigInfo.java
@@ -0,0 +1,22 @@
+package org.dreeam.leaf.config.annotations;
+
+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;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface ConfigInfo {
+
+ String name();
+
+ /**
+ * Optional option comment. Text blocks are supported.
+ *
+ * Supply one value for all locales, or English and Chinese values in that order.
+ */
+ String[] comments() default {};
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/DoNotLoad.java b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/DoNotLoad.java
index b6687584b7..58d48e9971 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/DoNotLoad.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/DoNotLoad.java
@@ -1,8 +1,20 @@
package org.dreeam.leaf.config.annotations;
+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;
+/**
+ * Marks a non-configuration field whose value is initialized by module lifecycle hooks from
+ * already loaded configuration fields.
+ *
+ * Fields annotated with {@code DoNotLoad} are never read from or written to configuration
+ * and should not be annotated with {@link ConfigInfo}.
+ */
+@Documented
@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
public @interface DoNotLoad {
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/HotReloadUnsupported.java b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/HotReloadUnsupported.java
index c89bf6a7ec..717407f8a6 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/HotReloadUnsupported.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/annotations/HotReloadUnsupported.java
@@ -1,8 +1,14 @@
package org.dreeam.leaf.config.annotations;
+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;
+/** Prevents an annotated field, or every configuration field in an annotated module, from being reloaded. */
+@Documented
@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.TYPE})
public @interface HotReloadUnsupported {
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/migration/ConfigPathMigration.java b/leaf-server/src/main/java/org/dreeam/leaf/config/migration/ConfigPathMigration.java
new file mode 100644
index 0000000000..88da7f20cc
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/migration/ConfigPathMigration.java
@@ -0,0 +1,38 @@
+package org.dreeam.leaf.config.migration;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
+
+import java.util.Objects;
+
+/**
+ * Moves a configuration value from a legacy path to its replacement path.
+ */
+public final class ConfigPathMigration {
+
+ private ConfigPathMigration() {
+ }
+
+ /**
+ * Moves the value at {@code oldPath} to {@code newPath} when the old path exists.
+ *
+ * A successful migration removes the old path, so it is performed only once when the
+ * configuration is next saved.
+ *
+ * @param config the configuration containing both paths
+ * @param oldPath the deprecated configuration path
+ * @param newPath the replacement configuration path
+ * @return {@code true} if a value was moved
+ */
+ public static boolean migrate(ConfigSection config, String oldPath, String newPath) {
+ Objects.requireNonNull(config, "config");
+ if (oldPath.equals(newPath)) {
+ throw new IllegalArgumentException("The old and new config paths must differ");
+ }
+
+ if (!config.contains(oldPath)) {
+ return false;
+ }
+ config.moveTo(oldPath, newPath);
+ return true;
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/migration/LeafConfigMigration.java b/leaf-server/src/main/java/org/dreeam/leaf/config/migration/LeafConfigMigration.java
new file mode 100644
index 0000000000..4e34ea40c4
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/migration/LeafConfigMigration.java
@@ -0,0 +1,109 @@
+package org.dreeam.leaf.config.migration;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
+
+import java.util.Objects;
+
+/**
+ * Applies versioned migrations to the loaded Leaf configuration instances before defaults are added.
+ */
+public final class LeafConfigMigration {
+
+ private LeafConfigMigration() {
+ }
+
+ public static void migrate(ConfigFile globalConfig, ConfigFile worldDefaultsConfig) throws Exception {
+ String storedVersion = globalConfig.getString("config-version", null);
+ MigrationContext context = new MigrationContext(globalConfig, worldDefaultsConfig);
+
+ applyMigrations(storedVersion, context);
+ }
+
+ private static void applyMigrations(String storedVersion, MigrationContext context) throws Exception {
+ /*
+ * Add migrations here, grouped by the config version that introduced the new path.
+ *
+ * if (LeafConfig.isConfigVersionBefore(storedVersion, "3.1")) {
+ * context.migrate(
+ * ConfigFileType.GLOBAL, "old.path",
+ * ConfigFileType.WORLD_DEFAULTS, "new.path"
+ * );
+ * }
+ */
+ }
+
+ private enum ConfigFileType {
+ GLOBAL,
+ WORLD_DEFAULTS
+ }
+
+ private static final class MigrationContext {
+
+ private final ConfigFile globalConfig;
+ private final ConfigFile worldDefaultsConfig;
+
+ private MigrationContext(ConfigFile globalConfig, ConfigFile worldDefaultsConfig) {
+ this.globalConfig = globalConfig;
+ this.worldDefaultsConfig = worldDefaultsConfig;
+ }
+
+ private void migrate(
+ ConfigFileType source,
+ String oldPath,
+ ConfigFileType target,
+ String newPath
+ ) throws Exception {
+ validateMigration(source, oldPath, target, newPath);
+
+ ConfigFile sourceConfig = config(source);
+ if (!sourceConfig.contains(oldPath)) {
+ return;
+ }
+
+ Object oldValue = sourceConfig.get(oldPath);
+ if (oldValue == null || oldValue instanceof ConfigSection) {
+ throw new IllegalStateException("Legacy config path must point to an option: "
+ + source + ":" + oldPath);
+ }
+
+ ConfigFile targetConfig = config(target);
+ if (targetConfig.contains(newPath)) {
+ sourceConfig.set(oldPath, null);
+ } else {
+ sourceConfig.moveTo(oldPath, newPath, targetConfig);
+ }
+ }
+
+ private static void validateMigration(
+ ConfigFileType source,
+ String oldPath,
+ ConfigFileType target,
+ String newPath
+ ) {
+ Objects.requireNonNull(source, "source");
+ requirePath(oldPath, "oldPath");
+ Objects.requireNonNull(target, "target");
+ requirePath(newPath, "newPath");
+
+ if (source == target && oldPath.equals(newPath)) {
+ throw new IllegalArgumentException("A migration must change the file or config path");
+ }
+ if (source == target
+ && (oldPath.startsWith(newPath + ".") || newPath.startsWith(oldPath + "."))) {
+ throw new IllegalArgumentException("Paths in the same config file must not overlap");
+ }
+ }
+
+ private static void requirePath(String path, String name) {
+ Objects.requireNonNull(path, name);
+ if (path.isBlank()) {
+ throw new IllegalArgumentException(name + " must not be blank");
+ }
+ }
+
+ private ConfigFile config(ConfigFileType type) {
+ return type == ConfigFileType.GLOBAL ? this.globalConfig : this.worldDefaultsConfig;
+ }
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/migration/gale/GaleConfigMigration.java b/leaf-server/src/main/java/org/dreeam/leaf/config/migration/gale/GaleConfigMigration.java
new file mode 100644
index 0000000000..989011a0dc
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/migration/gale/GaleConfigMigration.java
@@ -0,0 +1,276 @@
+package org.dreeam.leaf.config.migration.gale;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.dreeam.leaf.config.ConfigModule;
+import org.dreeam.leaf.config.LeafConfig;
+import org.dreeam.leaf.config.LeafWorldConfig;
+import org.dreeam.leaf.config.WorldConfigModule;
+import org.dreeam.leaf.config.util.ConfigFileIO;
+import org.dreeam.leaf.config.util.ConfigPaths;
+import org.jspecify.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Migrates the removed Gale configuration files into Leaf configuration modules.
+ */
+public final class GaleConfigMigration {
+
+ private static final Logger LOGGER = LogManager.getLogger(GaleConfigMigration.class.getSimpleName());
+ private static final String GLOBAL_FILE = "gale-global.yml";
+ private static final String WORLD_DEFAULTS_FILE = "gale-world-defaults.yml";
+ private static final String WORLD_OVERRIDE_FILE = "gale-world.yml";
+ private static final Set IGNORED_PATHS = Set.of("_version");
+
+ private static Path configDirectory;
+ private static List globalMappings = List.of();
+ private static List worldMappings = List.of();
+ private static Map resolvedWorldMappings = Map.of();
+
+ public static void migrate(Path directory, ConfigFile leafGlobalConfig, ConfigFile leafWorldDefaultsConfig) {
+ configDirectory = Objects.requireNonNull(directory, "directory").normalize();
+
+ Path globalPath = configDirectory.resolve(GLOBAL_FILE);
+ Path worldDefaultsPath = configDirectory.resolve(WORLD_DEFAULTS_FILE);
+
+ registerMappings();
+ Map resolvedGlobalMappings = resolveMappings(globalMappings);
+ resolvedWorldMappings = resolveMappings(worldMappings);
+ migrateValues(globalPath, resolvedGlobalMappings, leafGlobalConfig);
+ migrateValues(worldDefaultsPath, resolvedWorldMappings, leafWorldDefaultsConfig);
+ }
+
+ private static void registerMappings() {
+ globalMappings = new ArrayList<>();
+ worldMappings = new ArrayList<>();
+
+ addGlobalMapping("small-optimizations.reduced-intervals.increase-time-statistics", org.dreeam.leaf.config.modules.opt.global.ReducedIntervals.class, "increaseTimeStatistics");
+ addGlobalMapping("small-optimizations.reduced-intervals.update-entity-line-of-sight", org.dreeam.leaf.config.modules.opt.global.ReducedIntervals.class, "updateEntityLineOfSight");
+ addGlobalMapping("gameplay-mechanics.enable-book-writing", org.dreeam.leaf.config.modules.gameplay.global.GameplayMechanics.class, "enableBookWriting");
+ addGlobalMapping("misc.verify-chat-order", org.dreeam.leaf.config.modules.network.global.ChatOrderVerification.class, "enabled");
+ addGlobalMapping("misc.premium-account-slow-login-timeout", org.dreeam.leaf.config.modules.network.global.PremiumAccountSlowLoginTimeout.class, "ticks");
+ addGlobalMapping("misc.keepalive.send-multiple", org.dreeam.leaf.config.modules.network.global.Keepalive.class, "sendMultiple");
+ addGlobalMapping("misc.last-tick-time-in-tps-command.enabled", org.dreeam.leaf.config.modules.misc.global.LastTickTimeInTpsCommand.class, "enabled");
+ addGlobalMapping("misc.last-tick-time-in-tps-command.add-oversleep", org.dreeam.leaf.config.modules.misc.global.LastTickTimeInTpsCommand.class, "addOversleep");
+ addGlobalMapping("log-to-console.invalid-statistics", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "invalidStatistics");
+ addGlobalMapping("log-to-console.ignored-advancements", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "ignoredAdvancements");
+ addGlobalMapping("log-to-console.set-block-in-far-chunk", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "setBlockInFarChunk");
+ addGlobalMapping("log-to-console.unrecognized-recipes", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "unrecognizedRecipes");
+ addGlobalMapping("log-to-console.legacy-material-initialization", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "legacyMaterialInitialization");
+ addGlobalMapping("log-to-console.null-id-disconnections", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "nullIdDisconnections");
+ addGlobalMapping("log-to-console.player-login-locations", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "playerLoginLocations");
+ addGlobalMapping("log-to-console.invalid-legacy-text-component", org.dreeam.leaf.config.modules.misc.global.LogToConsole.class, "invalidLegacyTextComponent");
+ addGlobalMapping("log-to-console.chat.empty-message-warning", org.dreeam.leaf.config.modules.misc.global.Chat.class, "emptyMessageWarning");
+ addGlobalMapping("log-to-console.chat.expired-message-warning", org.dreeam.leaf.config.modules.misc.global.Chat.class, "expiredMessageWarning");
+ addGlobalMapping("log-to-console.chat.not-secure-marker", org.dreeam.leaf.config.modules.misc.global.Chat.class, "notSecureMarker");
+ addGlobalMapping("log-to-console.plugin-library-loader.downloads", org.dreeam.leaf.config.modules.misc.global.PluginLibraryLoader.class, "downloads");
+ addGlobalMapping("log-to-console.plugin-library-loader.start-load-libraries-for-plugin", org.dreeam.leaf.config.modules.misc.global.PluginLibraryLoader.class, "startLoadLibrariesForPlugin");
+ addGlobalMapping("log-to-console.plugin-library-loader.library-loaded", org.dreeam.leaf.config.modules.misc.global.PluginLibraryLoader.class, "libraryLoaded");
+
+ addWorldMapping("small-optimizations.save-fireworks", org.dreeam.leaf.config.modules.opt.world.SaveFireworks.class, "enabled");
+ addWorldMapping("small-optimizations.use-optimized-sheep-offspring-color", org.dreeam.leaf.config.modules.opt.world.OptimizedSheepOffspringColor.class, "enabled");
+ addWorldMapping("small-optimizations.max-projectile-chunk-loads.per-tick", org.dreeam.leaf.config.modules.opt.world.MaxProjectileChunkLoads.class, "perTick");
+ addWorldMapping("small-optimizations.max-projectile-chunk-loads.per-projectile.max", org.dreeam.leaf.config.modules.opt.world.MaxProjectileChunkLoads.class, "perProjectileMax");
+ addWorldMapping("small-optimizations.max-projectile-chunk-loads.per-projectile.reset-movement-after-reach-limit", org.dreeam.leaf.config.modules.opt.world.MaxProjectileChunkLoads.class, "perProjectileResetMovementAfterReachLimit");
+ addWorldMapping("small-optimizations.max-projectile-chunk-loads.per-projectile.remove-from-world-after-reach-limit", org.dreeam.leaf.config.modules.opt.world.MaxProjectileChunkLoads.class, "perProjectileRemoveFromWorldAfterReachLimit");
+ addWorldMapping("small-optimizations.reduced-intervals.check-stuck-in-wall", org.dreeam.leaf.config.modules.opt.world.ReducedIntervals.class, "checkStuckInWall");
+ addWorldMapping("small-optimizations.reduced-intervals.villager-item-repickup", org.dreeam.leaf.config.modules.opt.world.ReducedIntervals.class, "villagerItemRepickup");
+ addWorldMapping("small-optimizations.load-chunks.to-spawn-phantoms", org.dreeam.leaf.config.modules.opt.world.LoadChunks.class, "toSpawnPhantoms");
+ addWorldMapping("small-optimizations.load-chunks.to-activate-climbing-entities", org.dreeam.leaf.config.modules.opt.world.LoadChunks.class, "toActivateClimbingEntities");
+ addWorldMapping("gameplay-mechanics.fixes.broadcast-crit-animations-as-the-entity-being-critted", org.dreeam.leaf.config.modules.fixes.world.Fixes.class, "broadcastCritAnimationsAsTheEntityBeingCritted");
+ addWorldMapping("gameplay-mechanics.fixes.mc-238526", org.dreeam.leaf.config.modules.fixes.world.Fixes.class, "mc238526");
+ addWorldMapping("gameplay-mechanics.fixes.mc-121706", org.dreeam.leaf.config.modules.fixes.world.Fixes.class, "mc121706");
+ addWorldMapping("gameplay-mechanics.entities-can-random-stroll-into-non-ticking-chunks", org.dreeam.leaf.config.modules.gameplay.world.RandomStrollIntoNonTickingChunks.class, "enabled");
+ addWorldMapping("gameplay-mechanics.entity-wake-up-duration-ratio-standard-deviation", org.dreeam.leaf.config.modules.opt.world.EntityWakeUpDuration.class, "ratioStandardDeviation");
+ addWorldMapping("gameplay-mechanics.hide-flames-on-entities-with-fire-resistance", org.dreeam.leaf.config.modules.gameplay.world.HideFlamesOnEntitiesWithFireResistance.class, "enabled");
+ addWorldMapping("gameplay-mechanics.try-respawn-ender-dragon-after-end-crystal-place", org.dreeam.leaf.config.modules.gameplay.world.EnderDragonRespawn.class, "tryAfterEndCrystalPlace");
+ }
+
+ private static void addGlobalMapping(String oldPath, Class extends ConfigModule> moduleClass, String fieldName) {
+ globalMappings.add(new Mapping(oldPath, moduleClass, fieldName));
+ }
+
+ private static void addWorldMapping(String oldPath, Class extends WorldConfigModule> moduleClass, String fieldName) {
+ worldMappings.add(new Mapping(oldPath, moduleClass, fieldName));
+ }
+
+ public static void finalizeMigration(MinecraftServer server) {
+ if (configDirectory == null) {
+ return;
+ }
+
+ boolean galeConfigFound = archive(configDirectory.resolve(GLOBAL_FILE), Path.of(GLOBAL_FILE));
+ galeConfigFound |= archive(configDirectory.resolve(WORLD_DEFAULTS_FILE), Path.of(WORLD_DEFAULTS_FILE));
+ for (ServerLevel level : server.getAllLevels()) {
+ Path worldDirectory = server.storageSource.getDimensionPath(level.dimension());
+ Path absoluteWorldDirectory = worldDirectory.toAbsolutePath().normalize();
+ Path workingDirectory = Path.of("").toAbsolutePath().normalize();
+ Path worldContext = absoluteWorldDirectory.startsWith(workingDirectory)
+ ? workingDirectory.relativize(absoluteWorldDirectory)
+ : absoluteWorldDirectory.subpath(0, absoluteWorldDirectory.getNameCount());
+ Path backupPath = Path.of("world-overrides").resolve(worldContext).resolve(WORLD_OVERRIDE_FILE);
+ galeConfigFound |= archive(worldDirectory.resolve(WORLD_OVERRIDE_FILE), backupPath);
+ }
+
+ if (galeConfigFound) {
+ LOGGER.warn(
+ "Gale configuration migration has finished. Please manually check the migrated Leaf configuration files for correctness."
+ );
+ }
+ }
+
+ /**
+ * Attempts to create a Leaf override from Gale values.
+ *
+ * @return the newly created Leaf override, or {@code null} when normal Leaf loading should continue
+ */
+ public static @Nullable LeafWorldConfig migrateWorldOverride(
+ Path worldDirectory,
+ File leafFile,
+ LeafWorldConfig defaults
+ ) {
+ Path leafPath = leafFile.toPath();
+ Path galePath = worldDirectory.resolve(WORLD_OVERRIDE_FILE);
+
+ if (!Files.isRegularFile(galePath)) {
+ return null;
+ }
+
+ boolean leafFileCreated = false;
+ try {
+ if (leafFile.isFile()) {
+ LOGGER.warn(
+ "Could not migrate Gale world config {} because Leaf world override {} already exists.",
+ galePath, leafPath
+ );
+ return null;
+ }
+
+ ConfigFile galeConfig = loadSrcConfig(galePath);
+ if (galeConfig == null) {
+ return null;
+ }
+ boolean hasConfigValues = false;
+ for (String path : galeConfig.getKeys(false, true)) {
+ if (!IGNORED_PATHS.contains(path)) {
+ hasConfigValues = true;
+ break;
+ }
+ }
+ if (!hasConfigValues) {
+ return null;
+ }
+
+ Files.createFile(leafPath);
+ leafFileCreated = true;
+ ConfigFile leafConfig = ConfigFileIO.load(leafFile);
+ applyMappings(galeConfig, resolvedWorldMappings, leafConfig);
+ LeafWorldConfig migrated = LeafConfig.loadWorldOverride(leafConfig, defaults);
+ migrated.saveConfig();
+ return migrated;
+ } catch (Exception exception) {
+ if (leafFileCreated) {
+ try {
+ Files.deleteIfExists(leafPath);
+ } catch (IOException cleanupException) {
+ exception.addSuppressed(cleanupException);
+ }
+ }
+ LOGGER.error(
+ "Failed to migrate Gale world config {} for {}; using Leaf world defaults.",
+ galePath, worldDirectory, exception
+ );
+ return null;
+ }
+ }
+
+ private static void migrateValues(Path srcPath, Map mappings, ConfigFile leafConfig) {
+ ConfigFile srcConfig = loadSrcConfig(srcPath);
+ if (srcConfig == null) {
+ return;
+ }
+ applyMappings(srcConfig, mappings, leafConfig);
+ }
+
+ private static void applyMappings(
+ ConfigFile srcConfig,
+ Map mappings,
+ ConfigFile leafConfig
+ ) {
+ for (Map.Entry entry : mappings.entrySet()) {
+ String oldPath = entry.getKey();
+ if (srcConfig.contains(oldPath)) {
+ leafConfig.set(entry.getValue(), srcConfig.get(oldPath));
+ }
+ }
+ }
+
+ private static Map resolveMappings(List mappings) {
+ Map resolvedMappings = new LinkedHashMap<>();
+ for (Mapping mapping : mappings) {
+ String oldPath = mapping.oldPath();
+ Field field;
+ try {
+ field = mapping.moduleClass().getDeclaredField(mapping.fieldName());
+ } catch (NoSuchFieldException exception) {
+ throw new IllegalArgumentException("Invalid Gale migration target "
+ + mapping.moduleClass().getName() + '.' + mapping.fieldName(), exception);
+ }
+
+ String leafPath = ConfigPaths.fieldPath(mapping.moduleClass(), field);
+
+ resolvedMappings.put(oldPath, leafPath);
+ }
+ return resolvedMappings;
+ }
+
+ private static @Nullable ConfigFile loadSrcConfig(Path srcPath) {
+ if (!Files.isRegularFile(srcPath)) {
+ return null;
+ }
+ try {
+ return ConfigFileIO.load(srcPath.toFile());
+ } catch (Exception exception) {
+ LOGGER.error("Failed to read Gale config {}; migration was skipped.", srcPath, exception);
+ return null;
+ }
+ }
+
+ private static boolean archive(Path srcPath, Path backupPath) {
+ if (!Files.isRegularFile(srcPath)) return false;
+ try {
+ Path relative = backupPath.normalize();
+ if (relative.isAbsolute() || relative.startsWith("..")) {
+ throw new IOException("Invalid Gale backup path: " + relative);
+ }
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddhhmmss");
+ Path backupDirectory = configDirectory.resolve("backup" + dateFormat.format(new Date()));
+ Path target = backupDirectory.resolve(relative).normalize();
+ Files.createDirectories(target.getParent());
+ Files.move(srcPath, target);
+ LOGGER.warn("Moved Gale config {} to {}.", srcPath, target);
+ } catch (IOException exception) {
+ LOGGER.error("Failed to back up Gale config {}; leaving it in place.", srcPath, exception);
+ }
+ return true;
+ }
+
+ private record Mapping(String oldPath, Class> moduleClass, String fieldName) {
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java
index 7672052a90..3bbddf6098 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java
@@ -1,32 +1,19 @@
package org.dreeam.leaf.config.modules.async;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class AsyncChunkSend extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.ASYNC.basePath() + ".async-chunk-send";
- }
+@HotReloadUnsupported
+@ConfigClassInfo(category = ConfigCategory.ASYNC, name = "async-chunk-send", comments = {
+ """
+ Makes chunk packet preparation and sending asynchronous to improve server performance.
+ This can significantly reduce main thread load when many players are loading chunks.""",
+ """
+ 使区块数据包准备和发送异步化以提高服务器性能.
+ 当许多玩家同时加载区块时, 这可以显著减少主线程负载."""
+})
+public class AsyncChunkSend implements ConfigModule {
+ @ConfigInfo(name = "enabled")
public static boolean enabled = false;
- private static boolean asyncChunkSendInitialized;
-
- @Override
- public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- Makes chunk packet preparation and sending asynchronous to improve server performance.
- This can significantly reduce main thread load when many players are loading chunks.""",
- """
- 使区块数据包准备和发送异步化以提高服务器性能.
- 当许多玩家同时加载区块时, 这可以显著减少主线程负载.""");
-
- if (asyncChunkSendInitialized) {
- globalConfig.getConfigSection(basePath());
- return;
- }
- asyncChunkSendInitialized = true;
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncMobSpawning.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncMobSpawning.java
index a352f06c86..dbb09ccb1b 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncMobSpawning.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncMobSpawning.java
@@ -1,37 +1,23 @@
package org.dreeam.leaf.config.modules.async;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class AsyncMobSpawning extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.ASYNC.basePath() + ".async-mob-spawning";
- }
+@HotReloadUnsupported
+@ConfigClassInfo(category = ConfigCategory.ASYNC, name = "async-mob-spawning", comments = {
+ """
+ Whether or not asynchronous mob spawning should be enabled.
+ On servers with many entities, this can improve performance by up to 15%. You must have
+ paper's per-player-mob-spawns setting set to true for this to work.
+ One quick note - this does not actually spawn mobs async (that would be very unsafe).
+ This just offloads some expensive calculations that are required for mob spawning.""",
+ """
+ 是否异步化生物生成.
+ 在实体较多的服务器上, 异步生成可最高带来 15% 的性能提升.
+ 须在Paper配置文件中打开 per-player-mob-spawns 才能生效."""
+})
+public class AsyncMobSpawning implements ConfigModule {
+ @ConfigInfo(name = "enabled")
public static boolean enabled = true;
- private static boolean asyncMobSpawningInitialized;
-
- @Override
- public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- Whether or not asynchronous mob spawning should be enabled.
- On servers with many entities, this can improve performance by up to 15%. You must have
- paper's per-player-mob-spawns setting set to true for this to work.
- One quick note - this does not actually spawn mobs async (that would be very unsafe).
- This just offloads some expensive calculations that are required for mob spawning.""",
- """
- 是否异步化生物生成.
- 在实体较多的服务器上, 异步生成可最高带来 15% 的性能提升.
- 须在Paper配置文件中打开 per-player-mob-spawns 才能生效.""");
-
- // This prevents us from changing the value during a reload.
- if (asyncMobSpawningInitialized) {
- globalConfig.getConfigSection(basePath());
- return;
- }
- asyncMobSpawningInitialized = true;
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPathfinding.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPathfinding.java
index a258311e62..86ec75d26c 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPathfinding.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPathfinding.java
@@ -1,66 +1,67 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.async.path.PathfindTaskRejectPolicy;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.LeafConfig;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class AsyncPathfinding extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.ASYNC.basePath() + ".async-pathfinding";
- }
+@HotReloadUnsupported
+@ConfigClassInfo(category = ConfigCategory.ASYNC, name = "async-pathfinding")
+public class AsyncPathfinding implements ConfigModule {
+ @ConfigInfo(name = "enabled")
public static boolean enabled = false;
- public static int asyncPathfindingMaxThreads = 0;
- public static int asyncPathfindingKeepalive = 60;
- public static int asyncPathfindingQueueSize = 0;
- public static PathfindTaskRejectPolicy asyncPathfindingRejectPolicy = PathfindTaskRejectPolicy.FLUSH_ALL;
- private static boolean asyncPathfindingInitialized;
+
+ @ConfigInfo(name = "max-threads")
+ public static int maxThreads = 0;
+
+ @ConfigInfo(name = "keepalive")
+ public static int keepalive = 60;
+
+ @ConfigInfo(name = "queue-size")
+ public static int queueSize = 0;
+
+ @ConfigInfo(name = "reject-policy", comments = {
+ """
+ The policy to use when the queue is full and a new task is submitted.
+ FLUSH_ALL: All pending tasks will be run on server thread.
+ CALLER_RUNS: Newly submitted task will be run on server thread.""",
+ """
+ 当队列满时, 新提交的任务将使用以下策略处理.
+ FLUSH_ALL: 所有等待中的任务都将在主线程上运行.
+ CALLER_RUNS: 新提交的任务将在主线程上运行."""
+ })
+ public static String rejectPolicy = "default";
+
+ public static @DoNotLoad PathfindTaskRejectPolicy rejectPolicyType = PathfindTaskRejectPolicy.CALLER_RUNS;
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath() + ".reject-policy", """
- The policy to use when the queue is full and a new task is submitted.
- FLUSH_ALL: All pending tasks will be run on server thread.
- CALLER_RUNS: Newly submitted task will be run on server thread.""",
- """
- 当队列满时, 新提交的任务将使用以下策略处理.
- FLUSH_ALL: 所有等待中的任务都将在主线程上运行.
- CALLER_RUNS: 新提交的任务将在主线程上运行."""
- );
- if (asyncPathfindingInitialized) {
- globalConfig.getConfigSection(basePath());
- return;
- }
- asyncPathfindingInitialized = true;
-
final int availableProcessors = Runtime.getRuntime().availableProcessors();
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
- asyncPathfindingMaxThreads = globalConfig.getInt(basePath() + ".max-threads", asyncPathfindingMaxThreads);
- asyncPathfindingKeepalive = globalConfig.getInt(basePath() + ".keepalive", asyncPathfindingKeepalive);
- asyncPathfindingQueueSize = globalConfig.getInt(basePath() + ".queue-size", asyncPathfindingQueueSize);
- if (asyncPathfindingMaxThreads <= 0) {
- asyncPathfindingMaxThreads = Math.max(availableProcessors / 4, 1);
+ if (maxThreads <= 0) {
+ maxThreads = Math.max(availableProcessors / 4, 1);
}
if (!enabled) {
- asyncPathfindingMaxThreads = 0;
+ maxThreads = 0;
}
- if (asyncPathfindingQueueSize <= 0) {
- asyncPathfindingQueueSize = asyncPathfindingMaxThreads * 256;
+ if (queueSize <= 0) {
+ queueSize = maxThreads * 256;
}
- asyncPathfindingRejectPolicy = PathfindTaskRejectPolicy.fromString(globalConfig.getString(basePath() + ".reject-policy",
- availableProcessors >= 12 && asyncPathfindingQueueSize < 512
+ String policyStr = rejectPolicy;
+
+ if ("default".equalsIgnoreCase(policyStr)) {
+ policyStr = availableProcessors >= 12 && queueSize < 512
? PathfindTaskRejectPolicy.FLUSH_ALL.toString()
- : PathfindTaskRejectPolicy.CALLER_RUNS.toString())
- );
+ : PathfindTaskRejectPolicy.CALLER_RUNS.toString();
+ }
+
+ rejectPolicyType = PathfindTaskRejectPolicy.fromString(policyStr);
if (enabled) {
- LeafConfig.LOGGER.info("Using {} threads for Async Pathfinding", asyncPathfindingMaxThreads);
+ LeafConfig.LOGGER.info("Using {} threads for Async Pathfinding", maxThreads);
org.dreeam.leaf.async.path.AsyncPathProcessor.init();
}
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java
index 141ec7385b..076d701d61 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java
@@ -1,25 +1,22 @@
package org.dreeam.leaf.config.modules.async;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class AsyncPlayerDataSave extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.ASYNC.basePath() + ".async-playerdata-save";
- }
+@HotReloadUnsupported
+@ConfigClassInfo(category = ConfigCategory.ASYNC, name = "async-playerdata-save", comments = {
+ """
+ Make PlayerData saving asynchronously.""",
+ """
+ 异步保存玩家数据."""
+})
+public class AsyncPlayerDataSave implements ConfigModule {
+ @ConfigInfo(name = "enabled")
public static boolean enabled = false;
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- Make PlayerData saving asynchronously.""",
- """
- 异步保存玩家数据.""");
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
-
if (enabled) {
org.dreeam.leaf.async.AsyncPlayerDataSaving.init();
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java
index 03145a6c2e..ac6fa135f8 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java
@@ -1,41 +1,30 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.async.tracker.AsyncTracker;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.LeafConfig;
-import org.dreeam.leaf.config.annotations.Experimental;
-
-public class MultithreadedTracker extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.ASYNC.basePath() + ".async-entity-tracker";
- }
-
- @Experimental
- public static boolean enabled = false;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@HotReloadUnsupported
+@ConfigClassInfo(category = ConfigCategory.ASYNC, name = "async-entity-tracker", comments = {
+ """
+ ** Experimental Feature **
+ Make entity tracking asynchronously, can improve performance significantly,
+ especially in some massive entities in small area situations.""",
+ """
+ ** 实验性功能 **
+ 异步实体跟踪,
+ 在实体数量多且密集的情况下效果明显."""
+})
+public class MultithreadedTracker implements ConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public static @Experimental boolean enabled = false;
+
+ @ConfigInfo(name = "threads")
public static int threads = 0;
- private static boolean asyncMultithreadedTrackerInitialized;
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- ** Experimental Feature **
- Make entity tracking asynchronously, can improve performance significantly,
- especially in some massive entities in small area situations.""", """
- ** 实验性功能 **
- 异步实体跟踪,
- 在实体数量多且密集的情况下效果明显.""");
-
- if (asyncMultithreadedTrackerInitialized) {
- globalConfig.getConfigSection(basePath());
- return;
- }
- asyncMultithreadedTrackerInitialized = true;
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", false);
- threads = globalConfig.getInt(basePath() + ".threads", 0);
-
if (threads <= 0) {
threads = Math.min(Runtime.getRuntime().availableProcessors(), 4);
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java
index 37949132f4..48e7d5bee3 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/SparklyPaperParallelWorldTicking.java
@@ -1,53 +1,56 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.async.world.UnsafeReadPolicy;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.LeafConfig;
-import org.dreeam.leaf.config.annotations.Experimental;
-
-public class SparklyPaperParallelWorldTicking extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.ASYNC.basePath() + ".parallel-world-ticking";
- }
-
- @Experimental
- public static boolean enabled = false;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.ASYNC, name = "parallel-world-ticking", comments = {
+ """
+ **Experimental feature**
+ Enables parallel world ticking to improve performance on multi-core systems.""",
+ """
+ **实验性功能**
+ 启用并行世界处理以提高多核 CPU 使用率."""
+})
+public class SparklyPaperParallelWorldTicking implements ConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public static @Experimental boolean enabled = false;
+
+ @ConfigInfo(name = "threads")
public static int threads = 8;
+
+ @ConfigInfo(name = "log-container-creation-stacktraces")
public static boolean logContainerCreationStacktraces = false;
+
+ @ConfigInfo(name = "disable-hard-throw")
public static boolean disableHardThrow = false;
+
+ @ConfigInfo(name = "async-unsafe-read-handling")
+ public static String asyncUnsafeReadHandling = "DISABLED";
+
+ public static @DoNotLoad UnsafeReadPolicy asyncUnsafeReadHandlingPolicy = UnsafeReadPolicy.DISABLED;
+
@Deprecated
- public static Boolean runAsyncTasksSync;
- public static UnsafeReadPolicy asyncUnsafeReadHandling = UnsafeReadPolicy.DISABLED;
+ public static @DoNotLoad Boolean runAsyncTasksSync;
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- **Experimental feature**
- Enables parallel world ticking to improve performance on multi-core systems.""",
- """
- **实验性功能**
- 启用并行世界处理以提高多核 CPU 使用率.""");
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
- threads = globalConfig.getInt(basePath() + ".threads", threads);
if (enabled) {
if (threads <= 0) threads = 8;
} else {
threads = 0;
}
- logContainerCreationStacktraces = globalConfig.getBoolean(basePath() + ".log-container-creation-stacktraces", logContainerCreationStacktraces);
logContainerCreationStacktraces = enabled && logContainerCreationStacktraces;
- disableHardThrow = globalConfig.getBoolean(basePath() + ".disable-hard-throw", disableHardThrow);
disableHardThrow = enabled && disableHardThrow;
- asyncUnsafeReadHandling = UnsafeReadPolicy.fromString(globalConfig.getString(basePath() + ".async-unsafe-read-handling", asyncUnsafeReadHandling.toString()));
- // Transfer old config
- runAsyncTasksSync = globalConfig.getBoolean(basePath() + ".run-async-tasks-sync");
+ asyncUnsafeReadHandlingPolicy = UnsafeReadPolicy.fromString(asyncUnsafeReadHandling);
+
+ // TODO: Transfer old config
+ runAsyncTasksSync = LeafConfig.globalConfig().getBoolean("async.parallel-world-ticking.run-async-tasks-sync");
if (runAsyncTasksSync != null && runAsyncTasksSync) {
- LeafConfig.LOGGER.warn("The setting '{}.run-async-tasks-sync' is deprecated, removed automatically. Use 'async-unsafe-read-handling: BUFFERED' for buffered reads instead.", basePath());
+ LeafConfig.LOGGER.warn("The setting 'async.parallel-world-ticking.run-async-tasks-sync' is deprecated, removed automatically. Use 'async-unsafe-read-handling: BUFFERED' for buffered reads instead.");
}
if (enabled) {
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/MCBugFix.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/MCBugFix.java
index acf24c52de..12dbd31586 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/MCBugFix.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/MCBugFix.java
@@ -2,58 +2,55 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class MCBugFix extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.FIXES.basePath() + ".vanilla-bug-fix";
- }
+@ConfigClassInfo(category = ConfigCategory.FIXES, name = "vanilla-bug-fix", comments = {
+ "Fixes for vanilla Minecraft bugs.",
+ "针对 Minecraft 原版的漏洞修复。"
+})
+public class MCBugFix implements ConfigModule {
public static final Logger LOGGER = LogManager.getLogger("Leaf Vanilla Bug Fix");
+ @ConfigInfo(name = "mc-152094", comments = {
+ """
+ Whether to fix the bug End City ship generation gets cut at chunk borders.
+ Mojira link: https://mojira.dev/MC-152094""",
+ """
+ 是否修复末地船在区块边缘生成时被截断的问题。
+ 漏洞跟踪器链接:https://mojira.dev/MC-152094"""
+ })
+ public static boolean mc152094 = false;
+
+ @ConfigInfo(name = "mc-270656", comments = {
+ """
+ Whether to fix incorrect granting of 'Who needs rockets?' advancement.
+ Mojira link: https://mojira.dev/MC-270656""",
+ """
+ 是否修复“还要啥火箭啊?”进度触发的错误检查逻辑。
+ 漏洞跟踪器链接:https://mojira.dev/MC-270656"""
+ })
public static boolean mc270656 = false;
+
+ @ConfigInfo(name = "mc-301114", comments = {
+ """
+ Whether to fix the memory leak in the combat tracker caused by the mob constantly being damaged.
+ Mojira link: https://mojira.dev/MC-301114""",
+ """
+ 是否修复战斗跟踪器(Combat Tracker)中,因生物受到持续性伤害导致的内存泄漏。
+ 漏洞跟踪器链接:https://mojira.dev/MC-301114"""
+ })
public static boolean mc301114 = false;
+
+ @ConfigInfo(name = "mc-301114-max-entries", comments = {
+ "Max allowed entries in mob's combat tracker.",
+ "生物战斗跟踪器中允许的最大条目。"
+ })
public static int mc301114maxCombatEntries = 10240;
- public static boolean mc152094 = false;
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(),
- "Fixes for vanilla Minecraft bugs.",
- "针对 Minecraft 原版的漏洞修复。"
- );
- mc270656 = globalConfig.getBoolean(basePath() + ".mc-270656", mc270656, globalConfig.pickStringRegionBased(
- """
- Whether to fix incorrect granting of 'Who needs rockets?' advancement.
- Mojira link: https://mojira.dev/MC-270656""",
- """
- 是否修复“还要啥火箭啊?”进度触发的错误检查逻辑。
- 漏洞跟踪器链接:https://mojira.dev/MC-270656"""
- ));
- mc301114 = globalConfig.getBoolean(basePath() + ".mc-301114", mc301114, globalConfig.pickStringRegionBased(
- """
- Whether to fix the memory leak in the combat tracker caused by the mob constantly being damaged.
- Mojira link: https://mojira.dev/MC-301114""",
- """
- 是否修复战斗跟踪器(Combat Tracker)中,因生物受到持续性伤害导致的内存泄漏。
- 漏洞跟踪器链接:https://mojira.dev/MC-301114"""
- ));
- mc301114maxCombatEntries = globalConfig.getInt(basePath() + ".mc-301114-max-entries", mc301114maxCombatEntries, globalConfig.pickStringRegionBased(
- "Max allowed entries in mob's combat tracker.",
- "生物战斗跟踪器中允许的最大条目。"
- ));
-
mc301114maxCombatEntries = Math.max(1, mc301114maxCombatEntries);
-
- mc152094 = globalConfig.getBoolean(basePath() + ".mc-152094", mc152094, globalConfig.pickStringRegionBased(
- """
- Whether to fix the bug End City ship generation gets cut at chunk borders.
- Mojira link: https://mojira.dev/MC-152094""",
- """
- 是否修复末地船在区块边缘生成时被截断的问题。
- 漏洞跟踪器链接:https://mojira.dev/MC-152094"""
- ));
}
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/PreventMoveIntoWeakLoadedChunks.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/PreventMoveIntoWeakLoadedChunks.java
index 54aa015ec5..46f8ff1ed0 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/PreventMoveIntoWeakLoadedChunks.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/PreventMoveIntoWeakLoadedChunks.java
@@ -1,36 +1,26 @@
package org.dreeam.leaf.config.modules.fixes;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-
-public class PreventMoveIntoWeakLoadedChunks extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.FIXES.basePath() + ".prevent-moving-into-weak-loaded-chunks";
- }
-
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.FIXES, name = "prevent-moving-into-weak-loaded-chunks", comments = {
+ "Prevents entities from moving into weak loaded chunks.",
+ "阻止实体进入弱加载区块。"
+})
+public class PreventMoveIntoWeakLoadedChunks implements ConfigModule {
+
+ @ConfigInfo(name = "enabled", comments = {
+ "Set to true to enable features below.",
+ "设置为 true 以启用以下功能。"
+ })
public static boolean enabled = false;
+ @ConfigInfo(name = "projectiles", comments = {
+ "Prevents projectiles from moving into weak loaded chunks.",
+ "阻止弹射物进入弱加载区块。"
+ })
public static boolean projectiles = false;
public static boolean isProjectileEnabled() {
return enabled && projectiles;
}
-
- @Override
- public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(),
- "Prevents entities from moving into weak loaded chunks.",
- "阻止实体进入弱加载区块。"
- );
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled, globalConfig.pickStringRegionBased(
- "Set to true to enable features below.",
- "设置为 true 以启用以下功能。"
- ));
-
- projectiles = globalConfig.getBoolean(basePath() + ".projectiles", projectiles, globalConfig.pickStringRegionBased(
- "Prevents projectiles from moving into weak loaded chunks.",
- "阻止弹射物进入弱加载区块。"
- ));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/world/Fixes.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/world/Fixes.java
new file mode 100644
index 0000000000..eca965c1f0
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/fixes/world/Fixes.java
@@ -0,0 +1,17 @@
+package org.dreeam.leaf.config.modules.fixes.world;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.FIXES, name = "gameplay-fixes")
+public final class Fixes implements WorldConfigModule {
+
+ @ConfigInfo(name = "broadcast-crit-animations-as-the-entity-being-critted")
+ public boolean broadcastCritAnimationsAsTheEntityBeingCritted = false;
+
+ @ConfigInfo(name = "mc-238526")
+ public boolean mc238526 = false;
+
+ @ConfigInfo(name = "mc-121706")
+ public boolean mc121706 = false;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/AfkCommand.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/AfkCommand.java
index 8832f22cac..7b9c4affe0 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/AfkCommand.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/AfkCommand.java
@@ -1,23 +1,18 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class AfkCommand extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".afk-command";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "afk-command")
+public class AfkCommand implements ConfigModule {
+ @ConfigInfo(name = "enabled", comments = {
+ """
+ The AFK command based on Minecraft built-in idle-timeout mechanism
+ Rest of AFK settings are in the Purpur config""",
+ """
+ 基于原版 idle-timeout 系统的 AFK 指令
+ 剩余配置项在 Purpur 配置里"""
+ })
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled, globalConfig.pickStringRegionBased("""
- The AFK command based on Minecraft built-in idle-timeout mechanism
- Rest of AFK settings are in the Purpur config""",
- """
- 基于原版 idle-timeout 系统的 AFK 指令
- 剩余配置项在 Purpur 配置里"""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/BookWriting.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/BookWriting.java
new file mode 100644
index 0000000000..4a26c8ed61
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/BookWriting.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.gameplay;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "book-writing")
+public final class BookWriting implements ConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public static boolean enabled = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableInventoryOverflowEvent.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableInventoryOverflowEvent.java
index 4cc1d5f667..4eb87903f2 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableInventoryOverflowEvent.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableInventoryOverflowEvent.java
@@ -1,32 +1,28 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class ConfigurableInventoryOverflowEvent extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".inventory-overflow-event";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "inventory-overflow-event")
+public class ConfigurableInventoryOverflowEvent implements ConfigModule {
+ @ConfigInfo(name = "enabled", comments = {
+ """
+ The event called when used plugin to Inventory#addItem
+ into player's inventory, and the inventory is full.
+ This is not recommended to use, please re-design to use the
+ returned map of Inventory#addItem method as soon as possible!""",
+ """
+ 此事件将在插件使用 Inventory#addItem 方法
+ 添加物品到玩家背包, 但是背包已满时调用.
+ 不建议使用此事件,请尽快迁移至使用 Inventory#addItem 方法
+ 返回的 map"""
+ })
public static boolean enabled = false;
- public static String listenerClass = "com.example.package.PlayerInventoryOverflowEvent";
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled, globalConfig.pickStringRegionBased("""
- The event called when used plugin to Inventory#addItem
- into player's inventory, and the inventory is full.
- This is not recommended to use, please re-design to use the
- returned map of Inventory#addItem method as soon as possible!""",
- """
- 此事件将在插件使用 Inventory#addItem 方法
- 添加物品到玩家背包, 但是背包已满时调用.
- 不建议使用此事件,请尽快迁移至使用 Inventory#addItem 方法
- 返回的 map"""));
- listenerClass = globalConfig.getString(basePath() + ".listener-class", listenerClass, globalConfig.pickStringRegionBased("""
- The full class name of the listener which listens to this inventory overflow event.""",
- """
- 监听此物品栏物品溢出事件的完整类名."""));
- }
+ @ConfigInfo(name = "listener-class", comments = {
+ "The full class name of the listener which listens to this inventory overflow event.",
+ "监听此物品栏物品溢出事件的完整类名."
+ })
+ public static String listenerClass = "com.example.package.PlayerInventoryOverflowEvent";
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableMaxUseItemDistance.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableMaxUseItemDistance.java
index 711deab050..aa9eeed1e4 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableMaxUseItemDistance.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableMaxUseItemDistance.java
@@ -1,28 +1,23 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class ConfigurableMaxUseItemDistance extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".player";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "player")
+public class ConfigurableMaxUseItemDistance implements ConfigModule {
+ @ConfigInfo(name = "max-use-item-distance", comments = {
+ """
+ The max distance of UseItem for players.
+ Set to -1 to disable max-distance-check.
+ NOTE: if set to -1 to disable the check,
+ players are able to use some packet modules of hack clients,
+ and NoCom Exploit!!""",
+ """
+ 玩家 UseItem 的最大距离.
+ 设置为 -1 来禁用最大距离检测.
+ 注意: 禁用此项后,
+ 玩家可以使用作弊客户端的部分发包模块和 NoCom 漏洞!!"""
+ })
public static double maxUseItemDistance = 1.0000001;
-
- @Override
- public void onLoaded() {
- maxUseItemDistance = globalConfig.getDouble(basePath() + ".max-use-item-distance", maxUseItemDistance, globalConfig.pickStringRegionBased("""
- The max distance of UseItem for players.
- Set to -1 to disable max-distance-check.
- NOTE: if set to -1 to disable the check,
- players are able to use some packet modules of hack clients,
- and NoCom Exploit!!""",
- """
- 玩家 UseItem 的最大距离.
- 设置为 -1 来禁用最大距离检测.
- 注意: 禁用此项后,
- 玩家可以使用作弊客户端的部分发包模块和 NoCom 漏洞!!"""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableTripWireDupe.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableTripWireDupe.java
index c7e7a75518..846e7ca106 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableTripWireDupe.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/ConfigurableTripWireDupe.java
@@ -1,18 +1,11 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class ConfigurableTripWireDupe extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath();
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY)
+public class ConfigurableTripWireDupe implements ConfigModule {
+ @ConfigInfo(name = "allow-tripwire-dupe")
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".allow-tripwire-dupe", enabled);
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/DeathItemDropKnockback.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/DeathItemDropKnockback.java
index b5cec8e94c..52560b101f 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/DeathItemDropKnockback.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/DeathItemDropKnockback.java
@@ -1,36 +1,26 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class DeathItemDropKnockback extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".death-item-drop-knockback";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "death-item-drop-knockback")
+public class DeathItemDropKnockback implements ConfigModule {
+ @ConfigInfo(name = "drop-around", comments = {
+ "If true, items will drop randomly around the player on death.",
+ "如果为 “true”,物品会在玩家死亡时随机掉落在其周围."
+ })
public static boolean dropAround = true;
- public static double horizontalForce = 0.5;
- public static double verticalForce = 0.2;
- @Override
- public void onLoaded() {
- dropAround = globalConfig.getBoolean(basePath() + ".drop-around", dropAround,
- globalConfig.pickStringRegionBased(
- "If true, items will drop randomly around the player on death.",
- "如果为 “true”,物品会在玩家死亡时随机掉落在其周围."
- ));
-
- horizontalForce = globalConfig.getDouble(basePath() + ".horizontal-force", horizontalForce,
- globalConfig.pickStringRegionBased(
- "Base speed for horizontal velocity when randomly dropping items.",
- "随机掉落物品时水平速度的基本速度."
- ));
+ @ConfigInfo(name = "horizontal-force", comments = {
+ "Base speed for horizontal velocity when randomly dropping items.",
+ "随机掉落物品时水平速度的基本速度."
+ })
+ public static double horizontalForce = 0.5;
- verticalForce = globalConfig.getDouble(basePath() + ".vertical-force", verticalForce,
- globalConfig.pickStringRegionBased(
- "Upward motion for randomly dropped items.",
- "随机掉落物品的向上运动."
- ));
- }
+ @ConfigInfo(name = "vertical-force", comments = {
+ "Upward motion for randomly dropped items.",
+ "随机掉落物品的向上运动."
+ })
+ public static double verticalForce = 0.2;
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/IceAndSnowChance.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/IceAndSnowChance.java
index 2038239217..6614113cdf 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/IceAndSnowChance.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/IceAndSnowChance.java
@@ -1,19 +1,16 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class IceAndSnowChance extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".ice-and-snow-chance";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY)
+public class IceAndSnowChance implements ConfigModule {
+ @ConfigInfo(name = "ice-and-snow-chance")
public static int iceAndSnowChance = 48;
@Override
public void onLoaded() {
- iceAndSnowChance = globalConfig.getInt(basePath(), iceAndSnowChance);
if (iceAndSnowChance <= 0) {
iceAndSnowChance = 48;
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/Knockback.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/Knockback.java
index 28501c22a3..e785a052c3 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/Knockback.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/Knockback.java
@@ -1,44 +1,35 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.annotations.Experimental;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class Knockback extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".knockback";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "knockback")
+public class Knockback implements ConfigModule {
+ @ConfigInfo(name = "snowball-knockback-players", comments = {
+ "Make snowball can knockback players.",
+ "使雪球可以击退玩家."
+ })
public static boolean snowballCanKnockback = false;
+
+ @ConfigInfo(name = "egg-knockback-players", comments = {
+ "Make egg can knockback players.",
+ "使鸡蛋可以击退玩家."
+ })
public static boolean eggCanKnockback = false;
+
+ @ConfigInfo(name = "can-player-knockback-zombie", comments = {
+ "Make players can knockback zombie.",
+ "使玩家可以击退僵尸."
+ })
public static boolean canPlayerKnockbackZombie = true;
- @Experimental
- public static boolean flushKnockback = false;
- public static boolean oldBlastProtectionKnockbackBehavior = false;
- @Override
- public void onLoaded() {
- snowballCanKnockback = globalConfig.getBoolean(basePath() + ".snowball-knockback-players", snowballCanKnockback,
- globalConfig.pickStringRegionBased(
- "Make snowball can knockback players.",
- "使雪球可以击退玩家."
- ));
- eggCanKnockback = globalConfig.getBoolean(basePath() + ".egg-knockback-players", eggCanKnockback,
- globalConfig.pickStringRegionBased(
- "Make egg can knockback players.",
- "使鸡蛋可以击退玩家."
- ));
- canPlayerKnockbackZombie = globalConfig.getBoolean(basePath() + ".can-player-knockback-zombie", canPlayerKnockbackZombie,
- globalConfig.pickStringRegionBased(
- "Make players can knockback zombie.",
- "使玩家可以击退僵尸."
- ));
- flushKnockback = globalConfig.getBoolean(basePath() + ".flush-location-while-knockback-player", flushKnockback,
- globalConfig.pickStringRegionBased(
- "Synchronize player immediately when knocked back.",
- "被击退时立即同步玩家."
- ));
- oldBlastProtectionKnockbackBehavior = globalConfig.getBoolean(basePath() + ".old-blast-protection-explosion-knockback", oldBlastProtectionKnockbackBehavior);
- }
+ @ConfigInfo(name = "flush-location-while-knockback-player", comments = {
+ "Synchronize player immediately when knocked back.",
+ "被击退时立即同步玩家."
+ })
+ public static @Experimental boolean flushKnockback = false;
+
+ @ConfigInfo(name = "old-blast-protection-explosion-knockback")
+ public static boolean oldBlastProtectionKnockbackBehavior = false;
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/OnlyPlayerPushable.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/OnlyPlayerPushable.java
index 56c8e1c56d..7aa021e986 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/OnlyPlayerPushable.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/OnlyPlayerPushable.java
@@ -1,21 +1,14 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class OnlyPlayerPushable extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".only-player-pushable";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY)
+public class OnlyPlayerPushable implements ConfigModule {
+ @ConfigInfo(name = "only-player-pushable", comments = {
+ "Enable to make only player pushable",
+ "是否只允许玩家被实体推动"
+ })
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath(), enabled, globalConfig.pickStringRegionBased(
- "Enable to make only player pushable",
- "是否只允许玩家被实体推动"
- ));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/SpawnerSettings.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/SpawnerSettings.java
index 534a8f3734..e88170314b 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/SpawnerSettings.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/SpawnerSettings.java
@@ -1,93 +1,73 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-
-public class SpawnerSettings extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".spawner-settings";
- }
-
- // Global toggle
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "spawner-settings", comments = {
+ "This section contains settings for mob spawner blocks.",
+ "此部分包含刷怪笼生物生成的设置."
+})
+public class SpawnerSettings implements ConfigModule {
+
+ @ConfigInfo(name = "enabled", comments = {
+ "Enable custom spawner settings. Set to true to enable all features below.",
+ "启用自定义刷怪笼设置. 设为 true 以启用以下所有功能."
+ })
public static boolean enabled = false;
- // Default values for spawner settings
+ @ConfigInfo(name = "checks.light-level-check", comments = {
+ "Check if there is the required light level to spawn the mob",
+ "检查是否有所需的光照等级来生成怪物"
+ })
public static boolean lightLevelCheck = false;
+
+ @ConfigInfo(name = "checks.spawner-max-nearby-check", comments = {
+ "Check if there are the max amount of nearby mobs to spawn the mob",
+ "检查附近是否已达到最大怪物数量限制"
+ })
public static boolean spawnerMaxNearbyCheck = true;
+
+ @ConfigInfo(name = "checks.check-for-nearby-players", comments = {
+ "Check if any players are in a radius to spawn the mob",
+ "检查是否有玩家在生成怪物的半径范围内"
+ })
public static boolean checkForNearbyPlayers = true;
+
+ @ConfigInfo(name = "checks.spawner-block-checks", comments = {
+ "Check if there are physical blocks obstructing the spawn location, or if custom spawn rules (isValidPosition) fail due to block conditions.",
+ "检查是否有物理方块阻挡生成位置, 或自定义生成规则(isValidPosition)因方块条件失败."
+ })
public static boolean spawnerBlockChecks = false;
+
+ @ConfigInfo(name = "checks.water-prevent-spawn-check", comments = {
+ "Checks if there is water around that prevents spawning",
+ "检查周围是否有水阻止生成"
+ })
public static boolean waterPreventSpawnCheck = false;
+
+ @ConfigInfo(name = "checks.ignore-spawn-rules", comments = {
+ "Ignore mob-specific spawn rules, like animals needing grass or specific biomes/blocks (does not affect light level or physical obstruction checks).",
+ "忽略特定于生物的生成规则, 例如动物需要草方块或特定的生物群系/方块 (不影响光照等级或物理障碍物检查)."
+ })
public static boolean ignoreSpawnRules = false;
+ @ConfigInfo(name = "min-spawn-delay", comments = {
+ "Minimum delay (in ticks) between spawner spawns. Higher values slow down spawners.",
+ "刷怪笼生成怪物之间的最小延迟 (以刻为单位). 较高的值会减缓刷怪笼的速度."
+ })
public static int minSpawnDelay = 200;
+
+ @ConfigInfo(name = "max-spawn-delay", comments = {
+ "Maximum delay (in ticks) between spawner spawns. Higher values slow down spawners.",
+ "刷怪笼生成怪物之间的最大延迟 (以刻为单位). 较高的值会减缓刷怪笼的速度."
+ })
public static int maxSpawnDelay = 800;
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(),
- "This section contains settings for mob spawner blocks.",
- "此部分包含刷怪笼生物生成的设置.");
-
- // Global toggle
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled,
- globalConfig.pickStringRegionBased(
- "Enable custom spawner settings. Set to true to enable all features below.",
- "启用自定义刷怪笼设置. 设为 true 以启用以下所有功能."
- ));
-
- // Checks section
globalConfig.addCommentRegionBased(basePath() + ".checks",
"Various checks that can be enabled or disabled for spawner blocks.",
- "可以为刷怪笼启用或禁用的各种检查.");
-
- lightLevelCheck = globalConfig.getBoolean(basePath() + ".checks.light-level-check", lightLevelCheck,
- globalConfig.pickStringRegionBased(
- "Check if there is the required light level to spawn the mob",
- "检查是否有所需的光照等级来生成怪物"
- ));
-
- spawnerMaxNearbyCheck = globalConfig.getBoolean(basePath() + ".checks.spawner-max-nearby-check", spawnerMaxNearbyCheck,
- globalConfig.pickStringRegionBased(
- "Check if there are the max amount of nearby mobs to spawn the mob",
- "检查附近是否已达到最大怪物数量限制"
- ));
-
- checkForNearbyPlayers = globalConfig.getBoolean(basePath() + ".checks.check-for-nearby-players", checkForNearbyPlayers,
- globalConfig.pickStringRegionBased(
- "Check if any players are in a radius to spawn the mob",
- "检查是否有玩家在生成怪物的半径范围内"
- ));
-
- spawnerBlockChecks = globalConfig.getBoolean(basePath() + ".checks.spawner-block-checks", spawnerBlockChecks,
- globalConfig.pickStringRegionBased(
- "Check if there are physical blocks obstructing the spawn location, or if custom spawn rules (isValidPosition) fail due to block conditions.",
- "检查是否有物理方块阻挡生成位置, 或自定义生成规则(isValidPosition)因方块条件失败."
- ));
-
- waterPreventSpawnCheck = globalConfig.getBoolean(basePath() + ".checks.water-prevent-spawn-check", waterPreventSpawnCheck,
- globalConfig.pickStringRegionBased(
- "Checks if there is water around that prevents spawning",
- "检查周围是否有水阻止生成"
- ));
- ignoreSpawnRules = globalConfig.getBoolean(basePath() + ".checks.ignore-spawn-rules", ignoreSpawnRules,
- globalConfig.pickStringRegionBased(
- "Ignore mob-specific spawn rules, like animals needing grass or specific biomes/blocks (does not affect light level or physical obstruction checks).",
- "忽略特定于生物的生成规则, 例如动物需要草方块或特定的生物群系/方块 (不影响光照等级或物理障碍物检查)."
- ));
-
- // Delay settings
-
- minSpawnDelay = globalConfig.getInt(basePath() + ".min-spawn-delay", minSpawnDelay,
- globalConfig.pickStringRegionBased(
- "Minimum delay (in ticks) between spawner spawns. Higher values slow down spawners.",
- "刷怪笼生成怪物之间的最小延迟 (以刻为单位). 较高的值会减缓刷怪笼的速度."
- ));
-
- maxSpawnDelay = globalConfig.getInt(basePath() + ".max-spawn-delay", maxSpawnDelay,
- globalConfig.pickStringRegionBased(
- "Maximum delay (in ticks) between spawner spawns. Higher values slow down spawners.",
- "刷怪笼生成怪物之间的最大延迟 (以刻为单位). 较高的值会减缓刷怪笼的速度."
- ));
+ "可以为刷怪笼启用或禁用的各种检查."
+ );
}
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/UseSpigotItemMergingMech.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/UseSpigotItemMergingMech.java
index 743fffa57a..1024692f06 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/UseSpigotItemMergingMech.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/UseSpigotItemMergingMech.java
@@ -1,18 +1,11 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class UseSpigotItemMergingMech extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".use-spigot-item-merging-mechanism";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY)
+public class UseSpigotItemMergingMech implements ConfigModule {
+ @ConfigInfo(name = "use-spigot-item-merging-mechanism")
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath(), enabled);
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/VanillaHopper.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/VanillaHopper.java
index f37574135f..6c237a11db 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/VanillaHopper.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/VanillaHopper.java
@@ -1,18 +1,11 @@
package org.dreeam.leaf.config.modules.gameplay;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class VanillaHopper extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.GAMEPLAY.basePath() + ".use-vanilla-hopper";
- }
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY)
+public class VanillaHopper implements ConfigModule {
+ @ConfigInfo(name = "use-vanilla-hopper")
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath(), enabled);
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/EnderDragonRespawn.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/EnderDragonRespawn.java
new file mode 100644
index 0000000000..7bb065a31b
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/EnderDragonRespawn.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.gameplay.world;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "ender-dragon-respawn")
+public final class EnderDragonRespawn implements WorldConfigModule {
+
+ @ConfigInfo(name = "try-after-end-crystal-place")
+ public boolean tryAfterEndCrystalPlace = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/HideFlamesOnEntitiesWithFireResistance.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/HideFlamesOnEntitiesWithFireResistance.java
new file mode 100644
index 0000000000..28ab1cfe74
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/HideFlamesOnEntitiesWithFireResistance.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.gameplay.world;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "hide-flames-on-entities-with-fire-resistance")
+public final class HideFlamesOnEntitiesWithFireResistance implements WorldConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public boolean enabled = false;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/RandomStrollIntoNonTickingChunks.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/RandomStrollIntoNonTickingChunks.java
new file mode 100644
index 0000000000..434e7df13d
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/gameplay/world/RandomStrollIntoNonTickingChunks.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.gameplay.world;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.GAMEPLAY, name = "random-stroll-into-non-ticking-chunks")
+public final class RandomStrollIntoNonTickingChunks implements WorldConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public boolean enabled = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ConnectionMessage.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ConnectionMessage.java
index d7aa3b97af..ae16a9443f 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ConnectionMessage.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ConnectionMessage.java
@@ -1,43 +1,40 @@
package org.dreeam.leaf.config.modules.misc;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-
-public class ConnectionMessage extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".connection-message";
- }
-
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "connection-message", comments = {
+ """
+ Connection message, using MiniMessage format, set to "default" to use vanilla join message.
+ available placeholders:
+ - player name
+ - player display name""",
+ """
+ 自定义加入 & 退出消息 (MiniMessage 格式), 设置为 'default' 将使用原版消息.
+ 可用的内置变量:
+ - 玩家名称
+ - 玩家显示名称"""
+})
+public class ConnectionMessage implements ConfigModule {
+
+ @ConfigInfo(name = "join.enabled")
public static boolean joinEnabled = true;
+ @ConfigInfo(name = "join.message", comments = {
+ "Join message of player",
+ "玩家加入服务器时的消息"
+ })
public static String joinMessage = "default";
+
+ @ConfigInfo(name = "quit.enabled")
public static boolean quitEnabled = true;
+ @ConfigInfo(name = "quit.message", comments = {
+ "Quit message of player",
+ "玩家退出服务器时的消息"
+ })
public static String quitMessage = "default";
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- Connection message, using MiniMessage format, set to "default" to use vanilla join message.
- available placeholders:
- - player name
- - player display name""",
- """
- 自定义加入 & 退出消息 (MiniMessage 格式), 设置为 'default' 将使用原版消息.
- 可用的内置变量:
- - 玩家名称
- - 玩家显示名称""");
-
- joinEnabled = globalConfig.getBoolean(basePath() + ".join.enabled", joinEnabled);
- joinMessage = globalConfig.getString(basePath() + ".join.message", joinMessage, globalConfig.pickStringRegionBased(
- "Join message of player",
- "玩家加入服务器时的消息"
- ));
-
- quitEnabled = globalConfig.getBoolean(basePath() + ".quit.enabled", quitEnabled);
- quitMessage = globalConfig.getString(basePath() + ".quit.message", quitMessage, globalConfig.pickStringRegionBased(
- "Quit message of player",
- "玩家退出服务器时的消息"));
-
// Legacy compatibility
// TODO: config migration
joinMessage = joinMessage
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/DisableWorldDataSaving.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/DisableWorldDataSaving.java
index 36b790b7d1..040e252446 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/DisableWorldDataSaving.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/DisableWorldDataSaving.java
@@ -1,31 +1,25 @@
package org.dreeam.leaf.config.modules.misc;
import net.minecraft.server.level.ServerLevel;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
import java.util.ArrayList;
import java.util.List;
-public class DisableWorldDataSaving extends ConfigModule {
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "disable-world-data-saving")
+public class DisableWorldDataSaving implements ConfigModule {
+ @ConfigInfo(name = "worlds", comments = {
+ """
+ Worlds listed here will skip world data persistence.
+ Changes in chunks/entities remain in memory until unload/restart and are not written to disk.""",
+ """
+ 此处列出的世界将跳过世界数据持久化。
+ 区块/实体更改仅保留在内存中直到卸载或重启,不会写入磁盘。"""
+ })
public static List worlds = new ArrayList<>();
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".disable-world-data-saving";
- }
-
- @Override
- public void onLoaded() {
- worlds = globalConfig.getList(basePath() + ".worlds", worlds,
- globalConfig.pickStringRegionBased("""
- Worlds listed here will skip world data persistence.
- Changes in chunks/entities remain in memory until unload/restart and are not written to disk.""",
- """
- 此处列出的世界将跳过世界数据持久化。
- 区块/实体更改仅保留在内存中直到卸载或重启,不会写入磁盘。"""));
- }
-
public static boolean shouldSkipSave(ServerLevel serverLevel) {
return !worlds.isEmpty() && worlds.contains(serverLevel.getWorld().getName());
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/Including5sInGetTPS.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/Including5sInGetTPS.java
new file mode 100644
index 0000000000..e5f633053e
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/Including5sInGetTPS.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.misc;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.MISC)
+public class Including5sInGetTPS implements ConfigModule {
+
+ @ConfigInfo(name = "including-5s-in-get-tps")
+ public static boolean enabled = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/Including5sIngetTPS.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/Including5sIngetTPS.java
deleted file mode 100644
index 6d01c04988..0000000000
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/Including5sIngetTPS.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.dreeam.leaf.config.modules.misc;
-
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-
-public class Including5sIngetTPS extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath();
- }
-
- public static boolean enabled = true;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".including-5s-in-get-tps", enabled);
- }
-}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/LagCompensation.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/LagCompensation.java
index 07b6e46e84..38f0ef2295 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/LagCompensation.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/LagCompensation.java
@@ -1,29 +1,24 @@
package org.dreeam.leaf.config.modules.misc;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class LagCompensation extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".lag-compensation";
- }
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "lag-compensation", comments = {
+ """
+ This section contains lag compensation features,
+ which could ensure basic playing experience during a lag.""",
+ """
+ 这部分包含滞后补偿功能,
+ 可以在卡顿情况下保障基本游戏体验."""
+})
+public class LagCompensation implements ConfigModule {
+ @ConfigInfo(name = "enabled")
public static boolean enabled = false;
- public static boolean enableForWater = false;
- public static boolean enableForLava = false;
- @Override
- public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- This section contains lag compensation features,
- which could ensure basic playing experience during a lag.""",
- """
- 这部分包含滞后补偿功能,
- 可以在卡顿情况下保障基本游戏体验.""");
+ @ConfigInfo(name = "enable-for-water")
+ public static boolean enableForWater = false;
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
- enableForWater = globalConfig.getBoolean(basePath() + ".enable-for-water", enableForWater);
- enableForLava = globalConfig.getBoolean(basePath() + ".enable-for-lava", enableForLava);
- }
+ @ConfigInfo(name = "enable-for-lava")
+ public static boolean enableForLava = false;
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RegionFormatConfig.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RegionFormatConfig.java
index 27d42aef24..42cd9bfc71 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RegionFormatConfig.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RegionFormatConfig.java
@@ -3,80 +3,71 @@
import abomination.LinearRegionFile;
import me.earthme.luminol.enums.EnumRegionFormat;
import me.earthme.luminol.utils.BufferedLinearRegionFileFlusher;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.annotations.DoNotLoad;
-import org.dreeam.leaf.config.annotations.HotReloadUnsupported;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
import org.dreeam.leaf.util.LeafConstants;
-public class RegionFormatConfig extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".region-format";
- }
-
- public static @HotReloadUnsupported String regionFormatName = "MCA";
- public static @HotReloadUnsupported int compressionLevel = 6;
- public static @HotReloadUnsupported int ioThreadCount = 6;
- public static @HotReloadUnsupported int ioFlushDelay = -1;
- public static @HotReloadUnsupported boolean linearUseVirtualThread = true;
+@HotReloadUnsupported
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "region-format", comments = {
+ """
+ Linear is a region format that uses zstd compression instead of zlib.
+ This format saves about 50% of disk space.
+ Read Leaf docs before using!""",
+ """
+ Linear 是一种使用 zstd 压缩而非 ZLIB 的区域格式.
+ 该格式可节省约 50% 的磁盘空间.
+ 使用前请阅读 Leaf 文档!"""
+})
+public class RegionFormatConfig implements ConfigModule {
+
+ @ConfigInfo(name = "format-name", comments = {
+ "Available region format names: MCA, B_LINEAR, LINEAR_V2",
+ "可用格式: MCA, B_LINEAR, LINEAR_V2"
+ })
+ public static String regionFormatName = "MCA";
+
+ @ConfigInfo(name = "compress-level")
+ public static int compressionLevel = 6;
+
+ @ConfigInfo(name = "io-thread-count")
+ public static int ioThreadCount = 6;
+
+ @ConfigInfo(name = "io-flush-delay")
+ public static int ioFlushDelay = -1;
+
+ @ConfigInfo(name = "linear-use-virtual-thread")
+ public static boolean linearUseVirtualThread = true;
public static @DoNotLoad EnumRegionFormat regionFormat = EnumRegionFormat.MCA;
public static @DoNotLoad BufferedLinearRegionFileFlusher blinearFlusher = null;
- private static boolean regionFormatLoaded = false;
-
public static boolean isReadOnlyMode() {
return LeafConstants.LINEAR_V2_READ_ONLY && regionFormat == EnumRegionFormat.LINEAR_V2;
}
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- Linear is a region format that uses zstd compression instead of zlib.
- This format saves about 50% of disk space.
- Read Leaf docs before using!""",
- """
- Linear 是一种使用 zstd 压缩而非 ZLIB 的区域格式.
- 该格式可节省约 50% 的磁盘空间.
- 使用前请阅读 Leaf 文档!""");
-
- if (regionFormatLoaded) {
- globalConfig.getConfigSection(basePath());
- return;
- }
- regionFormatLoaded = true;
-
- regionFormatName = globalConfig.getString(basePath() + ".format-name", regionFormatName,
- globalConfig.pickStringRegionBased(
- "Available region format names: MCA, B_LINEAR, LINEAR_V2",
- "可用格式: MCA, B_LINEAR, LINEAR_V2"));
- compressionLevel = globalConfig.getInt(basePath() + ".compress-level", compressionLevel);
- ioThreadCount = globalConfig.getInt(basePath() + ".io-thread-count", ioThreadCount);
- ioFlushDelay = globalConfig.getInt(basePath() + ".io-flush-delay", ioFlushDelay);
- linearUseVirtualThread = globalConfig.getBoolean(basePath() + ".linear-use-virtual-thread", linearUseVirtualThread);
-
regionFormat = EnumRegionFormat.fromString(regionFormatName);
if (regionFormat == EnumRegionFormat.UNKNOWN) {
- LOGGER.error("Unknown region format type {}! Falling back to MCA format.", regionFormatName);
+ LeafConfig.LOGGER.error("Unknown region format type {}! Falling back to MCA format.", regionFormatName);
regionFormat = EnumRegionFormat.MCA;
return;
}
if (regionFormat == EnumRegionFormat.LINEAR_V2) {
checkCompressionLevel();
- LOGGER.warn("Linear v2 region format is unstable and not recommended to use, beware of data loss and take backups.");
+ LeafConfig.LOGGER.warn("Linear v2 region format is unstable and not recommended to use, beware of data loss and take backups.");
if (isReadOnlyMode()) {
- LOGGER.error("============================================================");
- LOGGER.error(" LINEAR_V2 READ-ONLY MODE ");
- LOGGER.error("============================================================");
- LOGGER.error("Linear v2 read-only mode is enabled.");
- LOGGER.error("Any world changes in Linear v2 regions will NOT be saved.");
- LOGGER.error("Chunk, entity, player data and POI changes will be discarded.");
- LOGGER.error("This mode is intended for inspection, testing, migration, or emergency recovery.");
- LOGGER.error("To enable LINEAR_V2 writing, stop the server, take backups,");
- LOGGER.error("then remove the JVM flag: -D{}=true", LeafConstants.LINEAR_V2_READ_ONLY_FLAG);
- LOGGER.error("============================================================");
+ LeafConfig.LOGGER.error("============================================================");
+ LeafConfig.LOGGER.error(" LINEAR_V2 READ-ONLY MODE ");
+ LeafConfig.LOGGER.error("============================================================");
+ LeafConfig.LOGGER.error("Linear v2 read-only mode is enabled.");
+ LeafConfig.LOGGER.error("Any world changes in Linear v2 regions will NOT be saved.");
+ LeafConfig.LOGGER.error("Chunk, entity, player data and POI changes will be discarded.");
+ LeafConfig.LOGGER.error("This mode is intended for inspection, testing, migration, or emergency recovery.");
+ LeafConfig.LOGGER.error("To enable LINEAR_V2 writing, stop the server, take backups,");
+ LeafConfig.LOGGER.error("then remove the JVM flag: -D{}=true", LeafConstants.LINEAR_V2_READ_ONLY_FLAG);
+ LeafConfig.LOGGER.error("============================================================");
}
LinearRegionFile.SAVE_DELAY_MS = ioFlushDelay <= 0 ? 100 : ioFlushDelay;
LinearRegionFile.SAVE_THREAD_MAX_COUNT = ioThreadCount;
@@ -96,8 +87,8 @@ public void onLoaded() {
private static void checkCompressionLevel() {
if (compressionLevel > 22 || compressionLevel < 1) {
- LOGGER.error("Linear or BufferedLinear region compression level should be between 1 and 22, but got {} in config", compressionLevel);
- LOGGER.error("Falling back to compression level 1.");
+ LeafConfig.LOGGER.error("Linear or BufferedLinear region compression level should be between 1 and 22, but got {} in config", compressionLevel);
+ LeafConfig.LOGGER.error("Falling back to compression level 1.");
compressionLevel = 1;
}
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveChangeNonEditableSignWarning.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveChangeNonEditableSignWarning.java
index 7d20ba0755..bab0416026 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveChangeNonEditableSignWarning.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveChangeNonEditableSignWarning.java
@@ -1,22 +1,14 @@
package org.dreeam.leaf.config.modules.misc;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class RemoveChangeNonEditableSignWarning extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath();
- }
+@ConfigClassInfo(category = ConfigCategory.MISC)
+public class RemoveChangeNonEditableSignWarning implements ConfigModule {
+ @ConfigInfo(name = "remove-change-non-editable-sign-warning", comments = {
+ "Enable to prevent console spam.",
+ "移除修改无法编辑的告示牌时输出的警告."
+ })
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".remove-change-non-editable-sign-warning", enabled,
- globalConfig.pickStringRegionBased(
- "Enable to prevent console spam.",
- "移除修改无法编辑的告示牌时输出的警告."
- ));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveSpigotCheckBungee.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveSpigotCheckBungee.java
index 815e6fcd34..612ba03db1 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveSpigotCheckBungee.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RemoveSpigotCheckBungee.java
@@ -1,22 +1,17 @@
package org.dreeam.leaf.config.modules.misc;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class RemoveSpigotCheckBungee extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".remove-spigot-check-bungee-config";
- }
+@ConfigClassInfo(category = ConfigCategory.MISC)
+public class RemoveSpigotCheckBungee implements ConfigModule {
+ @ConfigInfo(name = "remove-spigot-check-bungee-config", comments = {
+ """
+ Enable player enter backend server through proxy
+ without backend server enabling its bungee mode.""",
+ """
+ 使服务器无需打开 bungee 模式即可让玩家加入后端服务器."""
+ })
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath(), enabled, globalConfig.pickStringRegionBased("""
- Enable player enter backend server through proxy
- without backend server enabling its bungee mode.""",
- """
- 使服务器无需打开 bungee 模式即可让玩家加入后端服务器."""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/SecureSeed.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/SecureSeed.java
deleted file mode 100644
index f06da7be8c..0000000000
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/SecureSeed.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.dreeam.leaf.config.modules.misc;
-
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-
-public class SecureSeed extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".secure-seed";
- }
-
- public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- Once you enable secure seed, all ores and structures are generated with 1024-bit seed
- instead of using 64-bit seed in vanilla, made seed cracker become impossible.""",
- """
- 安全种子开启后, 所有矿物与结构都将使用1024位的种子进行生成, 无法被破解.""");
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
- }
-}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/SentryDSN.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/SentryDSN.java
index 6e1eb51f22..23b00c941b 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/SentryDSN.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/SentryDSN.java
@@ -1,40 +1,41 @@
package org.dreeam.leaf.config.modules.misc;
import org.apache.logging.log4j.Level;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class SentryDSN extends ConfigModule {
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "sentry")
+public class SentryDSN implements ConfigModule {
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".sentry";
- }
+ public static @DoNotLoad String sentryDsnConfigPath = "misc.sentry.dsn";
- public static String sentryDsnConfigPath;
+ @ConfigInfo(name = "dsn", comments = {
+ """
+ Sentry DSN for improved error logging, leave blank to disable,
+ Obtain from https://sentry.io/""",
+ """
+ Sentry DSN (出现严重错误时将发送至配置的Sentry DSN地址) (留空关闭)"""
+ })
public static String sentryDsn = "";
+
+ @ConfigInfo(name = "log-level", comments = {
+ "Logs with a level higher than or equal to this level will be recorded.",
+ "大于等于该等级的日志会被记录."
+ })
public static String logLevel = "WARN";
+
+ @ConfigInfo(name = "only-log-thrown", comments = {
+ "Only log with a Throwable will be recorded after enabling this.",
+ "是否仅记录带有 Throwable 的日志."
+ })
public static boolean onlyLogThrown = true;
@Override
public void onLoaded() {
String sentryEnvironment = System.getenv("SENTRY_DSN");
- String sentryConfig = globalConfig.getString(sentryDsnConfigPath = basePath() + ".dsn", sentryDsn, globalConfig.pickStringRegionBased("""
- Sentry DSN for improved error logging, leave blank to disable,
- Obtain from https://sentry.io/""",
- """
- Sentry DSN (出现严重错误时将发送至配置的Sentry DSN地址) (留空关闭)"""));
-
- sentryDsn = sentryEnvironment == null
- ? sentryConfig
- : sentryEnvironment;
- logLevel = globalConfig.getString(basePath() + ".log-level", logLevel, globalConfig.pickStringRegionBased("""
- Logs with a level higher than or equal to this level will be recorded.""",
- """
- 大于等于该等级的日志会被记录."""));
- onlyLogThrown = globalConfig.getBoolean(basePath() + ".only-log-thrown", onlyLogThrown, globalConfig.pickStringRegionBased("""
- Only log with a Throwable will be recorded after enabling this.""",
- """
- 是否仅记录带有 Throwable 的日志."""));
+ if (sentryEnvironment != null) {
+ sentryDsn = sentryEnvironment;
+ }
if (sentryDsn != null && !sentryDsn.isBlank()) {
gg.pufferfish.pufferfish.sentry.SentryManager.init(Level.getLevel(logLevel));
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ServerBrand.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ServerBrand.java
index 84dea545d5..013ffa8d7a 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ServerBrand.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/ServerBrand.java
@@ -1,20 +1,14 @@
package org.dreeam.leaf.config.modules.misc;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class ServerBrand extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".rebrand";
- }
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "rebrand")
+public class ServerBrand implements ConfigModule {
+ @ConfigInfo(name = "server-mod-name")
public static String serverModName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName();
- public static String serverGUIName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName() + " Console";
- @Override
- public void onLoaded() {
- serverModName = globalConfig.getString(basePath() + ".server-mod-name", serverModName);
- serverGUIName = globalConfig.getString(basePath() + ".server-gui-name", serverGUIName);
- }
+ @ConfigInfo(name = "server-gui-name")
+ public static String serverGUIName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName() + " Console";
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/UnknownCommandMessage.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/UnknownCommandMessage.java
index b6c75afe46..d032883159 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/UnknownCommandMessage.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/UnknownCommandMessage.java
@@ -1,27 +1,22 @@
package org.dreeam.leaf.config.modules.misc;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class UnknownCommandMessage extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".message";
- }
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "message")
+public class UnknownCommandMessage implements ConfigModule {
+ @ConfigInfo(name = "unknown-command", comments = {
+ """
+ Unknown command message, using MiniMessage format, set to "default" to use vanilla message,
+ placeholder:
+ , show message of the command exception.
+ , shows detail of the command exception.""",
+ """
+ 发送未知命令时的消息, 使用 MiniMessage 格式, 设置为 "default" 使用原版消息.
+ 变量:
+ , 显示命令错误所附提示消息.
+ , 显示命令错误详细信息."""
+ })
public static String unknownCommandMessage = "default";
-
- @Override
- public void onLoaded() {
- unknownCommandMessage = globalConfig.getString(basePath() + ".unknown-command", unknownCommandMessage, globalConfig.pickStringRegionBased("""
- Unknown command message, using MiniMessage format, set to "default" to use vanilla message,
- placeholder:
- , show message of the command exception.
- , shows detail of the command exception.""",
- """
- 发送未知命令时的消息, 使用 MiniMessage 格式, 设置为 "default" 使用原版消息.
- 变量:
- , 显示命令错误所附提示消息.
- , 显示命令错误详细信息."""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/VanillaUsernameCheck.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/VanillaUsernameCheck.java
index 8bdd2ed330..a6f79a8936 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/VanillaUsernameCheck.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/VanillaUsernameCheck.java
@@ -1,74 +1,83 @@
package org.dreeam.leaf.config.modules.misc;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.LeafConfig;
-import org.dreeam.leaf.config.annotations.Experimental;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
import java.util.regex.Pattern;
-public class VanillaUsernameCheck extends ConfigModule {
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "vanilla-username-check")
+public class VanillaUsernameCheck implements ConfigModule {
- public String basePath() {
- return ConfigCategory.MISC.basePath() + ".vanilla-username-check";
- }
+ @ConfigInfo(name = "remove-all-check", comments = {
+ """
+ Remove Vanilla username check,
+ allowing all characters as username.
+ WARNING: UNSAFE, USE AT YOUR OWN RISK!""",
+ """
+ 移除原版的用户名验证,
+ 让所有字符均可作为玩家名.
+ 警告: 完全移除验证非常不安全, 使用风险自负!"""
+ })
+ public static @Deprecated boolean removeAllCheck = false;
- @Deprecated
- public static boolean removeAllCheck = false;
+ @ConfigInfo(name = "enforce-skull-validation", comments = {
+ """
+ Enforce skull validation,
+ preventing skulls with invalid names from disconnecting the client.""",
+ """
+ 强制启用头颅验证,
+ 避免所有者带有特殊字符的头颅导致客户端掉线."""
+ })
public static boolean enforceSkullValidation = true;
- @Experimental
- public static boolean allowOldPlayersJoin = false;
+
+ @ConfigInfo(name = "allow-old-players-join", comments = {
+ """
+ Allow old players to join the server after the username regex is changed,
+ even if their names don't meet the new requirements.""",
+ """
+ 允许老玩家加入修改用户名验证正则后的服务器,
+ 即使他们的用户名不满足修改后的正则."""
+ })
+ public static @Experimental boolean allowOldPlayersJoin = false;
+
+ @ConfigInfo(name = "use-username-regex", comments = {
+ """
+ Use username regex to validate usernames,
+ allowing only characters specified in the regex.""",
+ """
+ 使用用户名正则来验证用户名,
+ 只允许正则指定的字符."""
+ })
public static boolean useUsernameRegex = false;
- private static final String defaultRegexString = "^[a-zA-Z0-9_.]*$";
- public static Pattern usernameRegex = Pattern.compile(defaultRegexString);
+
+ @ConfigInfo(name = "username-regex", comments = {
+ """
+ Username regex,
+ specifying the characters allowed in usernames.
+ Default: ^[a-zA-Z0-9_.]*$""",
+ """
+ 用户名正则,
+ 指定允许在用户名中使用的字符.
+ 默认: ^[a-zA-Z0-9_.]*$"""
+ })
+ private static String usernameRegexString = "^[a-zA-Z0-9_.]*$";
+
+ public static @DoNotLoad Pattern usernameRegex;
+
public static boolean shouldSkipNonPlayerNameCheck() { // helper
return removeAllCheck || useUsernameRegex;
}
@Override
public void onLoaded() {
- removeAllCheck = globalConfig.getBoolean(basePath() + ".remove-all-check", removeAllCheck, globalConfig.pickStringRegionBased("""
- Remove Vanilla username check,
- allowing all characters as username.
- WARNING: UNSAFE, USE AT YOUR OWN RISK!""",
- """
- 移除原版的用户名验证,
- 让所有字符均可作为玩家名.
- 警告: 完全移除验证非常不安全, 使用风险自负!"""));
- enforceSkullValidation = globalConfig.getBoolean(basePath() + ".enforce-skull-validation", enforceSkullValidation, globalConfig.pickStringRegionBased("""
- Enforce skull validation,
- preventing skulls with invalid names from disconnecting the client.""",
- """
- 强制启用头颅验证,
- 避免所有者带有特殊字符的头颅导致客户端掉线."""));
- allowOldPlayersJoin = globalConfig.getBoolean(basePath() + ".allow-old-players-join", allowOldPlayersJoin, globalConfig.pickStringRegionBased("""
- Allow old players to join the server after the username regex is changed,
- even if their names don't meet the new requirements.""",
- """
- 允许老玩家加入修改用户名验证正则后的服务器,
- 即使他们的用户名不满足修改后的正则."""));
- useUsernameRegex = globalConfig.getBoolean(basePath() + ".use-username-regex", useUsernameRegex, globalConfig.pickStringRegionBased("""
- Use username regex to validate usernames,
- allowing only characters specified in the regex.""",
- """
- 使用用户名正则来验证用户名,
- 只允许正则指定的字符."""));
- String regexString = globalConfig.getString(basePath() + ".username-regex", defaultRegexString, globalConfig.pickStringRegionBased(
- """
- Username regex,
- specifying the characters allowed in usernames.
- Default: %s""".formatted(defaultRegexString),
- """
- 用户名正则,
- 指定允许在用户名中使用的字符.
- 默认: %s""".formatted(defaultRegexString)));
- if (!regexString.isBlank()) {
+ if (!usernameRegexString.isBlank()) {
try {
- usernameRegex = Pattern.compile(regexString);
+ usernameRegex = Pattern.compile(usernameRegexString);
} catch (Exception e) {
- LeafConfig.LOGGER.error("Invalid username regex {} found, falling back to default.", regexString, e);
+ LeafConfig.LOGGER.error("Invalid username regex {} found, falling back to default.", usernameRegexString, e);
}
}
+
if (useUsernameRegex && removeAllCheck) {
LeafConfig.LOGGER.warn("Found conflicting configuration, remove-all-check and use-username-regex cannot be enabled at same time, ignoring remove-all-check...");
removeAllCheck = false;
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/Chat.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/Chat.java
new file mode 100644
index 0000000000..7f8590d47f
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/Chat.java
@@ -0,0 +1,17 @@
+package org.dreeam.leaf.config.modules.misc.global;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "chat-logging")
+public final class Chat implements ConfigModule {
+
+ @ConfigInfo(name = "empty-message-warning")
+ public static boolean emptyMessageWarning = false;
+
+ @ConfigInfo(name = "expired-message-warning")
+ public static boolean expiredMessageWarning = false;
+
+ @ConfigInfo(name = "not-secure-marker")
+ public static boolean notSecureMarker = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/LastTickTimeInTpsCommand.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/LastTickTimeInTpsCommand.java
new file mode 100644
index 0000000000..565f0e7488
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/LastTickTimeInTpsCommand.java
@@ -0,0 +1,14 @@
+package org.dreeam.leaf.config.modules.misc.global;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "last-tick-time-in-tps-command")
+public final class LastTickTimeInTpsCommand implements ConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public static boolean enabled = false;
+
+ @ConfigInfo(name = "add-oversleep")
+ public static boolean addOversleep = false;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/LogToConsole.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/LogToConsole.java
new file mode 100644
index 0000000000..aa404fad75
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/LogToConsole.java
@@ -0,0 +1,33 @@
+package org.dreeam.leaf.config.modules.misc.global;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "log-to-console")
+public final class LogToConsole implements ConfigModule {
+
+ @ConfigInfo(name = "invalid-statistics")
+ public static boolean invalidStatistics = true;
+
+ @ConfigInfo(name = "ignored-advancements")
+ public static boolean ignoredAdvancements = true;
+
+ @ConfigInfo(name = "set-block-in-far-chunk")
+ public static boolean setBlockInFarChunk = true;
+
+ @ConfigInfo(name = "unrecognized-recipes")
+ public static boolean unrecognizedRecipes = false;
+
+ @ConfigInfo(name = "legacy-material-initialization")
+ public static boolean legacyMaterialInitialization = false;
+
+ @ConfigInfo(name = "null-id-disconnections")
+ public static boolean nullIdDisconnections = true;
+
+ @ConfigInfo(name = "player-login-locations")
+ public static boolean playerLoginLocations = true;
+
+ @ConfigInfo(name = "invalid-legacy-text-component")
+ public static boolean invalidLegacyTextComponent = true;
+
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/PluginLibraryLoader.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/PluginLibraryLoader.java
new file mode 100644
index 0000000000..06619d6999
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/global/PluginLibraryLoader.java
@@ -0,0 +1,25 @@
+package org.dreeam.leaf.config.modules.misc.global;
+
+import org.bukkit.plugin.java.JavaPluginLoader;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "plugin-library-loader")
+public final class PluginLibraryLoader implements ConfigModule {
+
+ @ConfigInfo(name = "downloads")
+ public static boolean downloads = true;
+
+ @ConfigInfo(name = "start-load-libraries-for-plugin")
+ public static boolean startLoadLibrariesForPlugin = true;
+
+ @ConfigInfo(name = "library-loaded")
+ public static boolean libraryLoaded = true;
+
+ @Override
+ public void onLoaded() {
+ JavaPluginLoader.logDownloads = downloads;
+ JavaPluginLoader.logStartLoadLibrariesForPlugin = startLoadLibrariesForPlugin;
+ JavaPluginLoader.logLibraryLoaded = libraryLoaded;
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/world/SecureSeed.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/world/SecureSeed.java
new file mode 100644
index 0000000000..34a0c3c3e9
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/world/SecureSeed.java
@@ -0,0 +1,21 @@
+package org.dreeam.leaf.config.modules.misc.world;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.MISC, name = "secure-seed", comments = {
+ """
+ Once you enable secure seed, all ores and structures are generated with 1024-bit seed
+ instead of using 64-bit seed in vanilla, made seed cracker become impossible.""",
+ """
+ 安全种子开启后, 所有矿物与结构都将使用1024位的种子进行生成, 无法被破解."""
+})
+public class SecureSeed implements WorldConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public boolean enabled = false;
+
+ public static boolean isEnabled() {
+ return LeafConfig.worldDefaultsConfig().secureSeed.enabled;
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/AlternativeJoin.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/AlternativeJoin.java
index 3dd7123fbd..3d8187e5ca 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/AlternativeJoin.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/AlternativeJoin.java
@@ -1,20 +1,14 @@
package org.dreeam.leaf.config.modules.network;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class AlternativeJoin extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.NETWORK.basePath();
- }
+@ConfigClassInfo(category = ConfigCategory.NETWORK)
+public class AlternativeJoin implements ConfigModule {
+ @ConfigInfo(name = "async-switch-state", comments = {
+ "Async switch connection state.",
+ "异步切换连接状态."
+ })
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".async-switch-state", enabled, globalConfig.pickStringRegionBased(
- "Async switch connection state.",
- "异步切换连接状态."));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ChatMessageSignature.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ChatMessageSignature.java
index 79cf9749b5..54fe2d1a43 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ChatMessageSignature.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ChatMessageSignature.java
@@ -1,24 +1,19 @@
package org.dreeam.leaf.config.modules.network;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class ChatMessageSignature extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.NETWORK.basePath();
- }
+@ConfigClassInfo(category = ConfigCategory.NETWORK)
+public class ChatMessageSignature implements ConfigModule {
+ @ConfigInfo(name = "chat-message-signature", comments = {
+ """
+ Whether or not enable chat message signature,
+ disable will prevent players to report chat messages.
+ And also disables the popup when joining a server without
+ 'secure chat', such as offline-mode servers.""",
+ """
+ 是否启用聊天签名, 禁用后玩家无法进行聊天举报."""
+ })
public static boolean enabled = true;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".chat-message-signature", enabled, globalConfig.pickStringRegionBased("""
- Whether or not enable chat message signature,
- disable will prevent players to report chat messages.
- And also disables the popup when joining a server without
- 'secure chat', such as offline-mode servers.""",
- """
- 是否启用聊天签名, 禁用后玩家无法进行聊天举报."""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ChatOrderVerification.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ChatOrderVerification.java
new file mode 100644
index 0000000000..1168ee4e4c
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ChatOrderVerification.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.network;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.NETWORK, name = "chat-order-verification")
+public final class ChatOrderVerification implements ConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public static boolean enabled = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/Keepalive.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/Keepalive.java
new file mode 100644
index 0000000000..5215a8a1e7
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/Keepalive.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.network;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.NETWORK, name = "keepalive")
+public final class Keepalive implements ConfigModule {
+
+ @ConfigInfo(name = "send-multiple")
+ public static boolean sendMultiple = false;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/OptimizeNonFlushPacketSending.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/OptimizeNonFlushPacketSending.java
index f2f9821658..021623b2d1 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/OptimizeNonFlushPacketSending.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/OptimizeNonFlushPacketSending.java
@@ -1,33 +1,28 @@
package org.dreeam.leaf.config.modules.network;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class OptimizeNonFlushPacketSending extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.NETWORK.basePath();
- }
+@ConfigClassInfo(category = ConfigCategory.NETWORK)
+public class OptimizeNonFlushPacketSending implements ConfigModule {
+ @ConfigInfo(name = "OptimizeNonFlushPacketSending", comments = {
+ """
+ WARNING: This option is NOT compatible with ProtocolLib and may cause
+ issues with other plugins that modify packet handling.
+
+ Optimizes non-flush packet sending by using Netty's lazyExecute method to avoid
+ expensive thread wakeup calls when scheduling packet operations.
+
+ Requires server restart to take effect.""",
+ """
+ 警告: 此选项与 ProtocolLib 不兼容, 并可能导致与其他修改数据包
+ 处理的插件出现问题.
+
+ 通过使用 Netty 的 lazyExecute 方法来优化非刷新数据包的发送,
+ 避免在调度数据包操作时进行昂贵的线程唤醒调用.
+
+ 需要重启服务器才能生效."""
+ })
public static boolean enabled = false;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".OptimizeNonFlushPacketSending", enabled, globalConfig.pickStringRegionBased("""
- WARNING: This option is NOT compatible with ProtocolLib and may cause
- issues with other plugins that modify packet handling.
-
- Optimizes non-flush packet sending by using Netty's lazyExecute method to avoid
- expensive thread wakeup calls when scheduling packet operations.
-
- Requires server restart to take effect.""",
- """
- 警告: 此选项与 ProtocolLib 不兼容, 并可能导致与其他修改数据包
- 处理的插件出现问题.
-
- 通过使用 Netty 的 lazyExecute 方法来优化非刷新数据包的发送,
- 避免在调度数据包操作时进行昂贵的线程唤醒调用.
-
- 需要重启服务器才能生效."""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/PremiumAccountSlowLoginTimeout.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/PremiumAccountSlowLoginTimeout.java
new file mode 100644
index 0000000000..993298999d
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/PremiumAccountSlowLoginTimeout.java
@@ -0,0 +1,11 @@
+package org.dreeam.leaf.config.modules.network;
+
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
+
+@ConfigClassInfo(category = ConfigCategory.NETWORK, name = "premium-account-slow-login-timeout")
+public final class PremiumAccountSlowLoginTimeout implements ConfigModule {
+
+ @ConfigInfo(name = "ticks")
+ public static int ticks = -1;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ProtocolSupport.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ProtocolSupport.java
index a2b3f96210..d6d1b57a9d 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ProtocolSupport.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/network/ProtocolSupport.java
@@ -1,57 +1,67 @@
package org.dreeam.leaf.config.modules.network;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
import org.dreeam.leaf.protocol.DoABarrelRollPackets;
import org.dreeam.leaf.protocol.DoABarrelRollProtocol;
import java.util.concurrent.ThreadLocalRandom;
-public class ProtocolSupport extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.NETWORK.basePath() + ".protocol-support";
- }
+@ConfigClassInfo(category = ConfigCategory.NETWORK, name = "protocol-support")
+public class ProtocolSupport implements ConfigModule {
+ @ConfigInfo(name = "strict-mode")
public static boolean strictMode = false;
+
+ @ConfigInfo(name = "jade-protocol")
public static boolean jadeProtocol = false;
+
+ @ConfigInfo(name = "appleskin-protocol")
public static boolean appleskinProtocol = false;
+
+ @ConfigInfo(name = "appleskin-protocol-sync-tick-interval")
public static int appleskinSyncTickInterval = 20;
+
+ @ConfigInfo(name = "asteorbar-protocol")
public static boolean asteorBarProtocol = false;
+
+ @ConfigInfo(name = "chatimage-protocol")
public static boolean chatImageProtocol = false;
+
+ @ConfigInfo(name = "xaero-map-protocol")
public static boolean xaeroMapProtocol = false;
+
+ @ConfigInfo(name = "xaero-map-server-id")
public static int xaeroMapServerID = ThreadLocalRandom.current().nextInt(); // Leaf - Faster Random
+
+ @ConfigInfo(name = "syncmatica-protocol")
public static boolean syncmaticaProtocol = false;
+
+ @ConfigInfo(name = "syncmatica-quota")
public static boolean syncmaticaQuota = false;
+
+ @ConfigInfo(name = "syncmatica-quota-limit")
public static int syncmaticaQuotaLimit = 40000000;
+ @ConfigInfo(name = "do-a-barrel-roll-protocol")
public static boolean doABarrelRollProtocol = false;
+
+ @ConfigInfo(name = "do-a-barrel-roll-allow-thrusting")
public static boolean doABarrelRollAllowThrusting = false;
+
+ @ConfigInfo(name = "do-a-barrel-roll-force-enabled")
public static boolean doABarrelRollForceEnabled = false;
+
+ @ConfigInfo(name = "do-a-barrel-roll-force-installed")
public static boolean doABarrelRollForceInstalled = false;
+
+ @ConfigInfo(name = "do-a-barrel-roll-installed-timeout")
public static int doABarrelRollInstalledTimeout = 40;
@Override
public void onLoaded() {
- strictMode = globalConfig.getBoolean(basePath() + ".strict-mode", strictMode);
- jadeProtocol = globalConfig.getBoolean(basePath() + ".jade-protocol", jadeProtocol);
- appleskinProtocol = globalConfig.getBoolean(basePath() + ".appleskin-protocol", appleskinProtocol);
- appleskinSyncTickInterval = globalConfig.getInt(basePath() + ".appleskin-protocol-sync-tick-interval", appleskinSyncTickInterval);
- asteorBarProtocol = globalConfig.getBoolean(basePath() + ".asteorbar-protocol", asteorBarProtocol);
- chatImageProtocol = globalConfig.getBoolean(basePath() + ".chatimage-protocol", chatImageProtocol);
- xaeroMapProtocol = globalConfig.getBoolean(basePath() + ".xaero-map-protocol", xaeroMapProtocol);
- xaeroMapServerID = globalConfig.getInt(basePath() + ".xaero-map-server-id", xaeroMapServerID);
- syncmaticaProtocol = globalConfig.getBoolean(basePath() + ".syncmatica-protocol", syncmaticaProtocol);
- syncmaticaQuota = globalConfig.getBoolean(basePath() + ".syncmatica-quota", syncmaticaQuota);
- syncmaticaQuotaLimit = globalConfig.getInt(basePath() + ".syncmatica-quota-limit", syncmaticaQuotaLimit);
-
org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol.init(syncmaticaProtocol);
- doABarrelRollProtocol = globalConfig.getBoolean(basePath() + ".do-a-barrel-roll-protocol", doABarrelRollProtocol);
- doABarrelRollAllowThrusting = globalConfig.getBoolean(basePath() + ".do-a-barrel-roll-allow-thrusting", doABarrelRollAllowThrusting);
- doABarrelRollForceEnabled = globalConfig.getBoolean(basePath() + ".do-a-barrel-roll-force-enabled", doABarrelRollForceEnabled);
- doABarrelRollForceInstalled = globalConfig.getBoolean(basePath() + ".do-a-barrel-roll-force-installed", doABarrelRollForceInstalled);
- doABarrelRollInstalledTimeout = globalConfig.getInt(basePath() + ".do-a-barrel-roll-installed-timeout", 0);
if (doABarrelRollInstalledTimeout <= 0) {
doABarrelRollInstalledTimeout = 40;
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/CheckSurvivalBeforeGrowth.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/CheckSurvivalBeforeGrowth.java
index d7ff1da795..0be418f678 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/CheckSurvivalBeforeGrowth.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/CheckSurvivalBeforeGrowth.java
@@ -1,22 +1,14 @@
package org.dreeam.leaf.config.modules.opt;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class CheckSurvivalBeforeGrowth extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.PERF.basePath() + ".check-survival-before-growth";
- }
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "check-survival-before-growth")
+public class CheckSurvivalBeforeGrowth implements ConfigModule {
+ @ConfigInfo(name = "cactus-check-survival", comments = {
+ "Check if a cactus can survive before growing.",
+ "在仙人掌生长前检查其是否能够存活。"
+ })
public static boolean cactusCheckSurvivalBeforeGrowth = false;
-
- @Override
- public void onLoaded() {
- cactusCheckSurvivalBeforeGrowth = globalConfig.getBoolean(basePath() + ".cactus-check-survival", cactusCheckSurvivalBeforeGrowth,
- globalConfig.pickStringRegionBased("""
- Check if a cactus can survive before growing.""",
- """
- 在仙人掌生长前检查其是否能够存活。"""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DespawnTime.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DespawnTime.java
index e77c414529..78a4ca3bce 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DespawnTime.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DespawnTime.java
@@ -1,26 +1,19 @@
package org.dreeam.leaf.config.modules.opt;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.annotations.Experimental;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class DespawnTime extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.PERF.basePath() + ".despawn-time";
- }
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "despawn-time")
+public class DespawnTime implements ConfigModule {
@Experimental
+ @ConfigInfo(name = "proactive-weak-loading-despawn", comments = {
+ """
+ Proactive despawn check for weak-loaded entities.
+ This is an experimental feature.""",
+ """
+ 启用主动弱加载实体消失检查,
+ 这是一个实验性功能。"""
+ })
public static boolean proactiveWeakLoading = false;
-
- @Override
- public void onLoaded() {
- proactiveWeakLoading = globalConfig.getBoolean(basePath() + ".proactive-weak-loading-despawn", proactiveWeakLoading,
- globalConfig.pickStringRegionBased("""
- Proactive despawn check for weak-loaded entities.
- This is an experimental feature.""",
- """
- 启用主动弱加载实体消失检查,
- 这是一个实验性功能。"""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DontSaveEntity.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DontSaveEntity.java
index f4d14107ef..a0620d0aa4 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DontSaveEntity.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DontSaveEntity.java
@@ -1,27 +1,22 @@
package org.dreeam.leaf.config.modules.opt;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class DontSaveEntity extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.PERF.basePath() + ".dont-save-entity";
- }
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "dont-save-entity")
+public class DontSaveEntity implements ConfigModule {
+ @ConfigInfo(name = "dont-save-primed-tnt", comments = {
+ """
+ Disable save primed tnt on chunk unloads.
+ Useful for redstone/technical servers, can prevent machines from being exploded by TNT,
+ when player disconnected caused by Internet issue.""",
+ """
+ 区块卸载时不保存掉落的方块和激活的 TNT,
+ 可以避免在玩家掉线时机器被炸毁."""
+ })
public static boolean dontSavePrimedTNT = false;
- public static boolean dontSaveFallingBlock = false;
- @Override
- public void onLoaded() {
- dontSavePrimedTNT = globalConfig.getBoolean(basePath() + ".dont-save-primed-tnt", dontSavePrimedTNT,
- globalConfig.pickStringRegionBased("""
- Disable save primed tnt on chunk unloads.
- Useful for redstone/technical servers, can prevent machines from being exploded by TNT,
- when player disconnected caused by Internet issue.""",
- """
- 区块卸载时不保存掉落的方块和激活的 TNT,
- 可以避免在玩家掉线时机器被炸毁."""));
- dontSaveFallingBlock = globalConfig.getBoolean(basePath() + ".dont-save-falling-block", dontSaveFallingBlock);
- }
+ @ConfigInfo(name = "dont-save-falling-block")
+ public static boolean dontSaveFallingBlock = false;
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DynamicActivationofBrain.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DynamicActivationofBrain.java
index d743cbd144..eddac6533b 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DynamicActivationofBrain.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/DynamicActivationofBrain.java
@@ -3,27 +3,67 @@
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.EntityType;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
-import org.dreeam.leaf.config.LeafConfig;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
-public class DynamicActivationofBrain extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.PERF.basePath() + ".dab";
- }
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "dab", comments = {
+ """
+ Optimizes entity brains when
+ they're far away from the player""",
+ """
+ 根据距离动态优化生物 AI"""
+})
+public class DynamicActivationofBrain implements ConfigModule {
+ @ConfigInfo(name = "enabled")
public static boolean enabled = false;
+
+ @ConfigInfo(name = "dont-enable-if-in-water", comments = {
+ """
+ After enabling this, non-aquatic entities in the water will not be affected by DAB.
+ This could fix entities suffocate in the water.""",
+ """
+ 启用此项后, 在水中的非水生生物将不会被 DAB 影响.
+ 可以避免距离玩家较远的生物在水里淹死."""
+ })
+ public static boolean dontEnableIfInWater = false;
+
+ @ConfigInfo(name = "start-distance", comments = {
+ """
+ This value determines how far away an entity has to be
+ from the player to start being effected by DEAR.""",
+ """
+ 生物距离玩家多少格 DAB 开始生效"""
+ })
public static double startDistance = 12.0;
- public static double startDistanceSquared;
+
+ @ConfigInfo(name = "max-tick-freq", comments = {
+ """
+ This value defines how often in ticks, the furthest entity
+ will get their pathfinders and behaviors ticked. 20 = 1s""",
+ """
+ 最远处的实体每隔多少刻tick一次"""
+ })
public static int maximumActivationPrio = 20;
+
+ @ConfigInfo(name = "activation-dist-mod", comments = {
+ """
+ This value defines how much distance modifies an entity's
+ tick frequency. freq = (distanceToPlayer^2) / (2^value)",
+ If you want further away entities to tick less often, use 7.
+ If you want further away entities to tick more often, try 9."""
+ })
public static double activationDistanceMod = 8.0;
- public static boolean dontEnableIfInWater = false;
+
+ @ConfigInfo(name = "blacklisted-entities", comments = {
+ "A list of entities to ignore for activation",
+ "不会被 DAB 影响的实体列表"
+ })
public static List blackedEntities = new ArrayList<>(Arrays.asList(
"villager",
"axolotl",
@@ -32,45 +72,15 @@ public String basePath() {
"goat"
));
+ public static @DoNotLoad double startDistanceSquared;
+
@Override
public void onLoaded() {
- globalConfig.addCommentRegionBased(basePath(), """
- Optimizes entity brains when
- they're far away from the player""",
- """
- 根据距离动态优化生物 AI""");
-
- enabled = globalConfig.getBoolean(basePath() + ".enabled", enabled);
- dontEnableIfInWater = globalConfig.getBoolean(basePath() + ".dont-enable-if-in-water", dontEnableIfInWater, globalConfig.pickStringRegionBased("""
- After enabling this, non-aquatic entities in the water will not be affected by DAB.
- This could fix entities suffocate in the water.""",
- """
- 启用此项后, 在水中的非水生生物将不会被 DAB 影响.
- 可以避免距离玩家较远的生物在水里淹死."""));
- startDistance = globalConfig.getDouble(basePath() + ".start-distance", startDistance, globalConfig.pickStringRegionBased("""
- This value determines how far away an entity has to be
- from the player to start being effected by DEAR.""",
- """
- 生物距离玩家多少格 DAB 开始生效"""));
- maximumActivationPrio = globalConfig.getInt(basePath() + ".max-tick-freq", maximumActivationPrio, globalConfig.pickStringRegionBased("""
- This value defines how often in ticks, the furthest entity
- will get their pathfinders and behaviors ticked. 20 = 1s""",
- """
- 最远处的实体每隔多少刻tick一次"""));
- activationDistanceMod = globalConfig.getDouble(basePath() + ".activation-dist-mod", activationDistanceMod, """
- This value defines how much distance modifies an entity's
- tick frequency. freq = (distanceToPlayer^2) / (2^value)",
- If you want further away entities to tick less often, use 7.
- If you want further away entities to tick more often, try 9.""");
- blackedEntities = globalConfig.getList(basePath() + ".blacklisted-entities", blackedEntities,
- globalConfig.pickStringRegionBased("A list of entities to ignore for activation",
- "不会被 DAB 影响的实体列表"));
-
startDistanceSquared = startDistance * startDistance;
}
@Override
- public void onPostLoaded() {
+ public void onRegistriesLoaded() {
for (EntityType> entityType : BuiltInRegistries.ENTITY_TYPE) {
entityType.dabEnabled = true; // reset all, before setting the ones to true
}
@@ -85,7 +95,7 @@ public void onPostLoaded() {
EntityType.byString(typeId).ifPresentOrElse(entityType ->
entityType.dabEnabled = false,
- () -> LeafConfig.LOGGER.warn("Skip unknown entity {}, in {}", name, basePath() + ".blacklisted-entities")
+ () -> LeafConfig.LOGGER.warn("Skip unknown entity {}, in performance.dab.blacklisted-entities", name)
);
}
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EnableCachedMTBEntityTypeConvert.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EnableCachedMTBEntityTypeConvert.java
index bf2f20be19..b7a4b1ecaf 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EnableCachedMTBEntityTypeConvert.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EnableCachedMTBEntityTypeConvert.java
@@ -1,21 +1,14 @@
package org.dreeam.leaf.config.modules.opt;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class EnableCachedMTBEntityTypeConvert extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.PERF.basePath();
- }
+@ConfigClassInfo(category = ConfigCategory.PERF)
+public class EnableCachedMTBEntityTypeConvert implements ConfigModule {
+ @ConfigInfo(name = "enable-cached-minecraft-to-bukkit-entitytype-convert", comments = {
+ "Whether to cache expensive CraftEntityType#minecraftToBukkit call.",
+ "是否缓存Minecraft到Bukkit的实体类型转换."
+ })
public static boolean enabled = true;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".enable-cached-minecraft-to-bukkit-entitytype-convert", enabled, globalConfig.pickStringRegionBased("""
- Whether to cache expensive CraftEntityType#minecraftToBukkit call.""",
- """
- 是否缓存Minecraft到Bukkit的实体类型转换."""));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EntityGoal.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EntityGoal.java
index 7775a10da3..bf13f8ab86 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EntityGoal.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/EntityGoal.java
@@ -1,28 +1,39 @@
package org.dreeam.leaf.config.modules.opt;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class EntityGoal extends ConfigModule {
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "entity-goal")
+public class EntityGoal implements ConfigModule {
- public String basePath() {
- return ConfigCategory.PERF.basePath() + ".entity-goal";
- }
+ @ConfigInfo(name = "start-tick-chance.nearest-attackable-target")
+ private static int configuredChanceTarget = -1;
+
+ @ConfigInfo(name = "start-tick-chance.follow-parent")
+ private static int configuredChanceFollowParent = -1;
+
+ @ConfigInfo(name = "start-tick-chance.avoid-entity")
+ private static int configuredChanceAvoidEntity = -1;
+
+ @ConfigInfo(name = "start-tick-chance.temptation")
+ private static int configuredChanceTempt = -1;
+
+ @ConfigInfo(name = "start-tick-chance.enderman-look-for-player")
+ private static int configuredChanceEndermanLookForPlayer = -1;
- public static int chanceTarget = -1; // only all <= 10
- public static int chanceFollowParent = -1;
- public static int chanceAvoidEntity = -1;
- public static int chanceTempt = -1;
- public static int chanceEndermanLookForPlayer = -1;
+ public static @DoNotLoad int chanceTarget = -1; // only all <= 10
+ public static @DoNotLoad int chanceFollowParent = -1;
+ public static @DoNotLoad int chanceAvoidEntity = -1;
+ public static @DoNotLoad int chanceTempt = -1;
+ public static @DoNotLoad int chanceEndermanLookForPlayer = -1;
@Override
public void onLoaded() {
- String path = basePath() + ".start-tick-chance";
- chanceTarget = globalConfig.getInt(path + ".nearest-attackable-target", -1);
- chanceFollowParent = globalConfig.getInt(path + ".follow-parent", -1);
- chanceAvoidEntity = globalConfig.getInt(path + ".avoid-entity", -1);
- chanceTempt = globalConfig.getInt(path + ".temptation", -1);
- chanceEndermanLookForPlayer = globalConfig.getInt(path + ".enderman-look-for-player", -1);
+ chanceTarget = configuredChanceTarget;
+ chanceFollowParent = configuredChanceFollowParent;
+ chanceAvoidEntity = configuredChanceAvoidEntity;
+ chanceTempt = configuredChanceTempt;
+ chanceEndermanLookForPlayer = configuredChanceEndermanLookForPlayer;
// expect nearest_attackable_target
if (chanceFollowParent < 1) {
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/FasterStructureGenFutureSequencing.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/FasterStructureGenFutureSequencing.java
index 2b855c4463..718fa7be4e 100644
--- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/FasterStructureGenFutureSequencing.java
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/FasterStructureGenFutureSequencing.java
@@ -1,21 +1,14 @@
package org.dreeam.leaf.config.modules.opt;
-import org.dreeam.leaf.config.ConfigModule;
-import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.*;
+import org.dreeam.leaf.config.annotations.*;
-public class FasterStructureGenFutureSequencing extends ConfigModule {
-
- public String basePath() {
- return ConfigCategory.PERF.basePath();
- }
+@ConfigClassInfo(category = ConfigCategory.PERF)
+public class FasterStructureGenFutureSequencing implements ConfigModule {
+ @ConfigInfo(name = "faster-structure-gen-future-sequencing", comments = {
+ "May cause the inconsistent order of future compose tasks.",
+ "更快的结构生成任务分段."
+ })
public static boolean enabled = true;
-
- @Override
- public void onLoaded() {
- enabled = globalConfig.getBoolean(basePath() + ".faster-structure-gen-future-sequencing", enabled,
- globalConfig.pickStringRegionBased(
- "May cause the inconsistent order of future compose tasks.",
- "更快的结构生成任务分段."));
- }
}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/global/ReducedIntervals.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/global/ReducedIntervals.java
new file mode 100644
index 0000000000..52a59b7281
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/global/ReducedIntervals.java
@@ -0,0 +1,22 @@
+package org.dreeam.leaf.config.modules.opt.global;
+
+import net.minecraft.server.level.ServerPlayer;
+import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.ConfigModule;
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "reduced-intervals")
+public final class ReducedIntervals implements ConfigModule {
+
+ @ConfigInfo(name = "increase-time-statistics")
+ public static int increaseTimeStatistics = 1;
+
+ @ConfigInfo(name = "update-entity-line-of-sight")
+ public static int updateEntityLineOfSight = 4;
+
+ @Override
+ public void onLoaded() {
+ ServerPlayer.increaseTimeStatisticsInterval = Math.max(1, increaseTimeStatistics);
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/EntityWakeUpDuration.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/EntityWakeUpDuration.java
new file mode 100644
index 0000000000..ae3bc2a3f1
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/EntityWakeUpDuration.java
@@ -0,0 +1,13 @@
+package org.dreeam.leaf.config.modules.opt.world;
+
+import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.WorldConfigModule;
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "entity-wake-up-duration")
+public final class EntityWakeUpDuration implements WorldConfigModule {
+
+ @ConfigInfo(name = "ratio-standard-deviation")
+ public double ratioStandardDeviation = 0.2;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/LoadChunks.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/LoadChunks.java
new file mode 100644
index 0000000000..bb1ce7c312
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/LoadChunks.java
@@ -0,0 +1,16 @@
+package org.dreeam.leaf.config.modules.opt.world;
+
+import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.WorldConfigModule;
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "load-chunks")
+public final class LoadChunks implements WorldConfigModule {
+
+ @ConfigInfo(name = "to-spawn-phantoms")
+ public boolean toSpawnPhantoms = false;
+
+ @ConfigInfo(name = "to-activate-climbing-entities")
+ public boolean toActivateClimbingEntities = false;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/MaxProjectileChunkLoads.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/MaxProjectileChunkLoads.java
new file mode 100644
index 0000000000..d8bbd5a046
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/MaxProjectileChunkLoads.java
@@ -0,0 +1,22 @@
+package org.dreeam.leaf.config.modules.opt.world;
+
+import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.WorldConfigModule;
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "max-projectile-chunk-loads")
+public final class MaxProjectileChunkLoads implements WorldConfigModule {
+
+ @ConfigInfo(name = "per-tick")
+ public int perTick = 10;
+
+ @ConfigInfo(name = "per-projectile.max")
+ public int perProjectileMax = 10;
+
+ @ConfigInfo(name = "per-projectile.reset-movement-after-reach-limit")
+ public boolean perProjectileResetMovementAfterReachLimit = false;
+
+ @ConfigInfo(name = "per-projectile.remove-from-world-after-reach-limit")
+ public boolean perProjectileRemoveFromWorldAfterReachLimit = false;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/OptimizedSheepOffspringColor.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/OptimizedSheepOffspringColor.java
new file mode 100644
index 0000000000..94f80f439c
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/OptimizedSheepOffspringColor.java
@@ -0,0 +1,13 @@
+package org.dreeam.leaf.config.modules.opt.world;
+
+import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.WorldConfigModule;
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "optimized-sheep-offspring-color")
+public final class OptimizedSheepOffspringColor implements WorldConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public boolean enabled = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/ReducedIntervals.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/ReducedIntervals.java
new file mode 100644
index 0000000000..981638eb07
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/ReducedIntervals.java
@@ -0,0 +1,16 @@
+package org.dreeam.leaf.config.modules.opt.world;
+
+import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.WorldConfigModule;
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "reduced-intervals")
+public final class ReducedIntervals implements WorldConfigModule {
+
+ @ConfigInfo(name = "check-stuck-in-wall")
+ public int checkStuckInWall = 10;
+
+ @ConfigInfo(name = "villager-item-repickup")
+ public int villagerItemRepickup = 100;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/SaveFireworks.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/SaveFireworks.java
new file mode 100644
index 0000000000..a9b17a9fbd
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/opt/world/SaveFireworks.java
@@ -0,0 +1,13 @@
+package org.dreeam.leaf.config.modules.opt.world;
+
+import org.dreeam.leaf.config.ConfigCategory;
+import org.dreeam.leaf.config.WorldConfigModule;
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+@ConfigClassInfo(category = ConfigCategory.PERF, name = "save-fireworks")
+public final class SaveFireworks implements WorldConfigModule {
+
+ @ConfigInfo(name = "enabled")
+ public boolean enabled = true;
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/package-info.java b/leaf-server/src/main/java/org/dreeam/leaf/config/package-info.java
new file mode 100644
index 0000000000..7ed863b702
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/package-info.java
@@ -0,0 +1,2 @@
+@org.jspecify.annotations.NullMarked
+package org.dreeam.leaf.config;
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/util/ConfigFileIO.java b/leaf-server/src/main/java/org/dreeam/leaf/config/util/ConfigFileIO.java
new file mode 100644
index 0000000000..512ed192e2
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/util/ConfigFileIO.java
@@ -0,0 +1,151 @@
+package org.dreeam.leaf.config.util;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jspecify.annotations.Nullable;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.YAMLException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * ConfigurationMaster file loading and atomic saving utilities.
+ */
+public final class ConfigFileIO {
+
+ private static final Logger LOGGER = LogManager.getLogger(ConfigFileIO.class.getSimpleName());
+ private static final int MAX_CODE_POINTS = 100 * 1024 * 1024;
+
+ public static ConfigFile load(File file) throws Exception {
+ Path path = file.toPath();
+ if (!Files.isRegularFile(path)) {
+ return ConfigFile.loadConfig(file);
+ }
+
+ LoaderOptions loaderOptions = new LoaderOptions();
+ loaderOptions.setCodePointLimit(MAX_CODE_POINTS); // Increase YAML file size limit
+ Yaml yaml = new Yaml(new SafeConstructor(loaderOptions));
+ try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
+ yaml.load(reader);
+ } catch (YAMLException exception) {
+ throw new IOException("Malformed YAML configuration: " + path, exception);
+ }
+ return ConfigFile.loadConfig(file);
+ }
+
+ public static void saveAtomically(ConfigFile... configs) throws Exception {
+ Set targets = new HashSet<>();
+ List serializedConfigs = new ArrayList<>(configs.length);
+ for (ConfigFile config : configs) {
+ Objects.requireNonNull(config, "config");
+ Path target = config.getFile().toPath().toAbsolutePath().normalize();
+ if (!targets.add(target)) {
+ throw new IllegalArgumentException("Cannot save the same config file twice: " + target);
+ }
+ serializedConfigs.add(new SerializedConfig(target, config.saveToString()));
+ }
+
+ List stagedConfigs = new ArrayList<>(serializedConfigs.size());
+ List temporaryFiles = new ArrayList<>(serializedConfigs.size() * 2);
+ try {
+ for (SerializedConfig serializedConfig : serializedConfigs) {
+ Path target = serializedConfig.target();
+ Path parent = Objects.requireNonNull(target.getParent(), "Config file has no parent: " + target);
+ Files.createDirectories(parent);
+
+ String prefix = "." + target.getFileName() + ".";
+ Path staged = Files.createTempFile(parent, prefix, ".tmp");
+ temporaryFiles.add(staged);
+
+ boolean targetExisted = Files.exists(target);
+ @Nullable Path rollback = null;
+ if (targetExisted) {
+ rollback = Files.createTempFile(parent, prefix, ".rollback");
+ temporaryFiles.add(rollback);
+ Files.copy(target, rollback, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
+ try {
+ Files.setPosixFilePermissions(staged, Files.getPosixFilePermissions(target));
+ } catch (UnsupportedOperationException ignored) {
+ }
+ }
+
+ Files.writeString(
+ staged,
+ serializedConfig.content(),
+ StandardCharsets.UTF_8,
+ StandardOpenOption.TRUNCATE_EXISTING,
+ StandardOpenOption.WRITE
+ );
+ stagedConfigs.add(new StagedConfig(target, staged, rollback, targetExisted));
+ }
+
+ int committed = 0;
+ try {
+ for (StagedConfig stagedConfig : stagedConfigs) {
+ Files.move(
+ stagedConfig.staged(),
+ stagedConfig.target(),
+ StandardCopyOption.ATOMIC_MOVE,
+ StandardCopyOption.REPLACE_EXISTING
+ );
+ committed++;
+ }
+ } catch (Exception exception) {
+ for (int index = committed - 1; index >= 0; index--) {
+ StagedConfig stagedConfig = stagedConfigs.get(index);
+ try {
+ if (stagedConfig.targetExisted()) {
+ Files.move(
+ Objects.requireNonNull(stagedConfig.rollback()),
+ stagedConfig.target(),
+ StandardCopyOption.ATOMIC_MOVE,
+ StandardCopyOption.REPLACE_EXISTING
+ );
+ } else {
+ Files.deleteIfExists(stagedConfig.target());
+ }
+ } catch (Exception rollbackException) {
+ exception.addSuppressed(rollbackException);
+ if (stagedConfig.rollback() != null) {
+ temporaryFiles.remove(stagedConfig.rollback());
+ LOGGER.error(
+ "Failed to restore config {}; rollback copy was left at {}.",
+ stagedConfig.target(), stagedConfig.rollback(), rollbackException
+ );
+ }
+ }
+ }
+ throw exception;
+ }
+ } finally {
+ for (Path temporaryFile : temporaryFiles) {
+ try {
+ Files.deleteIfExists(temporaryFile);
+ } catch (IOException exception) {
+ LOGGER.warn("Failed to delete temporary config file {}.", temporaryFile, exception);
+ }
+ }
+ }
+ }
+
+ private record SerializedConfig(Path target, String content) {
+ }
+
+ private record StagedConfig(Path target, Path staged, @Nullable Path rollback, boolean targetExisted) {
+ }
+}
diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/util/ConfigPaths.java b/leaf-server/src/main/java/org/dreeam/leaf/config/util/ConfigPaths.java
new file mode 100644
index 0000000000..7109003c01
--- /dev/null
+++ b/leaf-server/src/main/java/org/dreeam/leaf/config/util/ConfigPaths.java
@@ -0,0 +1,31 @@
+package org.dreeam.leaf.config.util;
+
+import org.dreeam.leaf.config.annotations.ConfigClassInfo;
+import org.dreeam.leaf.config.annotations.ConfigInfo;
+
+import java.lang.reflect.Field;
+
+/** Resolves annotation-driven module and option paths. */
+public final class ConfigPaths {
+
+ public static String modulePath(Class> moduleClass) {
+ ConfigClassInfo info = moduleClass.getAnnotation(ConfigClassInfo.class);
+ if (info == null) {
+ throw new IllegalStateException("Configuration module " + moduleClass.getName()
+ + " is missing @ConfigClassInfo");
+ }
+
+ return info.name().isEmpty()
+ ? info.category().basePath()
+ : info.category().basePath() + '.' + info.name();
+ }
+
+ public static String fieldPath(Class> moduleClass, Field field) {
+ ConfigInfo info = field.getAnnotation(ConfigInfo.class);
+ if (info == null) {
+ throw new IllegalStateException("Configuration field " + field + " is missing @ConfigInfo");
+ }
+
+ return modulePath(moduleClass) + '.' + info.name();
+ }
+}
diff --git a/leaf-server/src/main/java/su/plo/matter/Globals.java b/leaf-server/src/main/java/su/plo/matter/Globals.java
index 11bf11174b..e19f2c103a 100644
--- a/leaf-server/src/main/java/su/plo/matter/Globals.java
+++ b/leaf-server/src/main/java/su/plo/matter/Globals.java
@@ -37,7 +37,7 @@ public enum Salt {
}
public static void setupGlobals(ServerLevel world) {
- if (!org.dreeam.leaf.config.modules.misc.SecureSeed.enabled) return;
+ if (!world.leafConfig().secureSeed.enabled) return;
long[] seed = world.getServer().getWorldGenSettings().options().featureSeed();
System.arraycopy(seed, 0, worldSeed, 0, WORLD_SEED_LONGS);