diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ec6c31a7..8674ca33 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -9,8 +9,9 @@ dependencies { api(libs.base.api) compileOnlyApi(projects.isolation) - annotationProcessor(libs.config.utils.ap) - api(libs.config.utils) + annotationProcessor(libs.configurate.`interface`.ap) + api(libs.configurate.`interface`) + implementation(libs.configurate.yaml) annotationProcessor(libs.database.utils.ap) api(libs.database.utils) diff --git a/core/src/main/java/org/geysermc/floodgate/core/config/ConfigAsPropertySource.java b/core/src/main/java/org/geysermc/floodgate/core/config/ConfigAsPropertySource.java index 5f349028..f9e566ea 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/config/ConfigAsPropertySource.java +++ b/core/src/main/java/org/geysermc/floodgate/core/config/ConfigAsPropertySource.java @@ -26,38 +26,36 @@ package org.geysermc.floodgate.core.config; import io.micronaut.context.env.PropertySource; -import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.geysermc.configutils.node.codec.strategy.object.ProxyEmbodimentStrategy; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.util.NamingSchemes; public final class ConfigAsPropertySource { private ConfigAsPropertySource() {} - public static PropertySource toPropertySource(FloodgateConfig config) { - Objects.requireNonNull(config); - return PropertySource.of(flatten(Map.of("config", config))); + public static PropertySource toPropertySource(ConfigurationNode rootNode) { + Objects.requireNonNull(rootNode); + + var root = CommentedConfigurationNode.root(); + root.node("config").from(rootNode.copy()); + + return PropertySource.of(flatten(root)); } - private static Map flatten(Map map) { - return flatten0(Map.of("", map).get("")); - } - - @SuppressWarnings("unchecked") - private static Map flatten0(Map map) { + private static Map flatten(ConfigurationNode node) { var result = new HashMap(); - map.forEach((key, value) -> { - if (Proxy.isProxyClass(value.getClass())) { - value = new ProxyEmbodimentStrategy().disembody(value); - } - if (value instanceof Map) { - flatten0((Map) value).forEach( - (key1, value1) -> result.put(key + "." + key1, value1) - ); + node.childrenMap().values().forEach(value -> { + // we expect the properties in camelCase + var key = NamingSchemes.CAMEL_CASE.coerce(value.key().toString()); + + if (value.isMap()) { + flatten(value).forEach((childKey, childValue) -> result.put(key + "." + childKey, childValue)); return; } - result.put(key, value); + result.put(key, value.raw()); }); return result; } diff --git a/core/src/main/java/org/geysermc/floodgate/core/config/ConfigLoader.java b/core/src/main/java/org/geysermc/floodgate/core/config/ConfigLoader.java index 5ed49499..c7768bf2 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/config/ConfigLoader.java +++ b/core/src/main/java/org/geysermc/floodgate/core/config/ConfigLoader.java @@ -25,12 +25,17 @@ package org.geysermc.floodgate.core.config; +import static org.spongepowered.configurate.NodePath.path; + import io.micronaut.context.ApplicationContext; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.UUID; -import org.geysermc.configutils.ConfigUtilities; -import org.geysermc.configutils.file.codec.PathFileCodec; -import org.geysermc.configutils.updater.change.Changes; +import org.geysermc.floodgate.core.util.Constants; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.interfaces.InterfaceDefaultOptions; +import org.spongepowered.configurate.transformation.ConfigurationTransformation; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; public final class ConfigLoader { private ConfigLoader() {} @@ -39,33 +44,38 @@ public final class ConfigLoader { public static T load(Path dataDirectory, boolean isProxy, ApplicationContext context) { var configClass = isProxy ? ProxyFloodgateConfig.class : FloodgateConfig.class; - // it would also be nice to have sections in versionBuilder so that you don't have to - // provide the path all the time - - ConfigUtilities utilities = - ConfigUtilities.builder() - .fileCodec(PathFileCodec.of(dataDirectory)) - .configFile("config.yml") - .changes(Changes.builder() - .version(1, Changes.versionBuilder() - .keyRenamed("playerLink.enable", "playerLink.enabled") - .keyRenamed("playerLink.allowLinking", "playerLink.allowed")) - .version(2, Changes.versionBuilder() - .keyRenamed("playerLink.useGlobalLinking", "playerLink.enableGlobalLinking")) - .version(3, Changes.versionBuilder() - .keyRenamed("playerLink.type", "database.type")) - .build()) - .definePlaceholder("metrics.uuid", UUID::randomUUID) - .postInitializeCallbackArgument(dataDirectory) - .build(); - + ConfigurationNode node; T config; try { - config = (T) utilities.executeOn(configClass); - } catch (Throwable throwable) { + var loader = YamlConfigurationLoader.builder() + .path(dataDirectory.resolve("config.yml")) + .defaultOptions(InterfaceDefaultOptions.get()) + .build(); + + node = loader.load(); + // temp fix for node.virtual() being broken + var virtual = !Files.exists(dataDirectory.resolve("config.yml")); + + var migrations = ConfigurationTransformation.versionedBuilder() + .addVersion(Constants.CONFIG_VERSION, twoToThree()) + .addVersion(2, oneToTwo()) + .addVersion(1, zeroToOne()) + .build(); + + var startVersion = migrations.version(node); + migrations.apply(node); + var endVersion = migrations.version(node); + + config = (T) node.get(configClass); + + // save default config or save migrated config + if (virtual || startVersion != endVersion) { + loader.save(node); + } + } catch (ConfigurateException exception) { throw new RuntimeException( "Failed to load the config! Try to delete the config file if this error persists", - throwable + exception ); } @@ -73,7 +83,34 @@ public final class ConfigLoader { context.registerSingleton(config); context.registerSingleton(FloodgateConfig.class, config); // make @Requires etc. work - context.getEnvironment().addPropertySource(ConfigAsPropertySource.toPropertySource(config)); + context.getEnvironment().addPropertySource(ConfigAsPropertySource.toPropertySource(node)); return config; } + + private static ConfigurationTransformation zeroToOne() { + return ConfigurationTransformation.builder() + .addAction(path("playerLink", "enable"), (path, value) -> { + return new Object[]{"playerLink", "enabled"}; + }) + .addAction(path("playerLink", "allowLinking"), (path, value) -> { + return new Object[]{"playerLink", "allowed"}; + }) + .build(); + } + + private static ConfigurationTransformation oneToTwo() { + return ConfigurationTransformation.builder() + .addAction(path("playerLink", "useGlobalLinking"), (path, value) -> { + return new Object[]{"playerLink", "enableGlobalLinking"}; + }) + .build(); + } + + private static ConfigurationTransformation twoToThree() { + return ConfigurationTransformation.builder() + .addAction(path("playerLink", "type"), (path, value) -> { + return new Object[]{"database", "type"}; + }) + .build(); + } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/config/FloodgateConfig.java b/core/src/main/java/org/geysermc/floodgate/core/config/FloodgateConfig.java index 3af22e76..495e0c4e 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/config/FloodgateConfig.java +++ b/core/src/main/java/org/geysermc/floodgate/core/config/FloodgateConfig.java @@ -25,43 +25,45 @@ package org.geysermc.floodgate.core.config; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.Key; -import org.geysermc.configutils.loader.callback.CallbackResult; -import org.geysermc.configutils.loader.callback.GenericPostInitializeCallback; -import org.geysermc.configutils.node.meta.Comment; -import org.geysermc.configutils.node.meta.ConfigSection; -import org.geysermc.configutils.node.meta.ConfigVersion; -import org.geysermc.configutils.node.meta.Defaults.DefaultBoolean; -import org.geysermc.configutils.node.meta.Defaults.DefaultNumeric; -import org.geysermc.configutils.node.meta.Defaults.DefaultString; -import org.geysermc.configutils.node.meta.Exclude; -import org.geysermc.configutils.node.meta.Hidden; -import org.geysermc.configutils.node.meta.Placeholder; +import java.util.UUID; +import org.geysermc.floodgate.core.util.Constants; +import org.spongepowered.configurate.interfaces.meta.Exclude; +import org.spongepowered.configurate.interfaces.meta.Field; +import org.spongepowered.configurate.interfaces.meta.Hidden; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; +import org.spongepowered.configurate.objectmapping.meta.PostProcess; /** * The global Floodgate configuration file used in every platform. Some platforms have their own * addition to the global configuration like {@link ProxyFloodgateConfig} for the proxies. */ -@ConfigVersion(3) -public interface FloodgateConfig extends GenericPostInitializeCallback { +@ConfigSerializable +public interface FloodgateConfig { + @Exclude default boolean proxy() { return this instanceof ProxyFloodgateConfig; } - @Override - default CallbackResult postInitialize(Path dataDirectory) { - Path keyPath = dataDirectory.resolve(keyFileName()); - - // don't assume that the key always exists with the existence of a config - if (!Files.exists(keyPath)) { - // TODO improve message and also link to article about corrupted keys/WinSCP/FTP - // Like, where is key.pem? - // TODO don't be so noisy with error. It makes it hard to understand what the error is. - return CallbackResult.failed("Floodgate requires a key file! " + - "Copy your key file from Geyser (key.pem) and paste it into " + keyPath); - } + @PostProcess + default void postInitialise() { + //todo add postInitialize with argument +// @PostProcess +// default void postInitialize(Path dataDirectory) throws SerializationException { +// Path keyPath = dataDirectory.resolve(keyFileName()); +// +// // don't assume that the key always exists with the existence of a config +// if (!Files.exists(keyPath)) { +// // TODO improve message and also link to article about corrupted keys/WinSCP/FTP +// // Like, where is key.pem? +// // TODO don't be so noisy with error. It makes it hard to understand what the error is. +// throw new SerializationException("Floodgate requires a key file! " + +// "Copy your key file from Geyser (key.pem) and paste it into " + keyPath); +// } //todo remove key file name config option @@ -71,25 +73,29 @@ public interface FloodgateConfig extends GenericPostInitializeCallback { if (usernamePrefix().length() >= 16) { usernamePrefix("."); } - - return CallbackResult.ok(); } - @Comment + @Comment(""" + In Floodgate bedrock player data is send encrypted. + The following value should point to the key Floodgate generated. + The public key should be used for the Geyser(s) and the private key for the Floodgate(s)""") @DefaultString("key.pem") String keyFileName(); - @Comment + @Comment(""" + Floodgate prepends a prefix to bedrock usernames to avoid conflicts + However, certain conflicts can cause issues with some plugins so this prefix is configurable using the property below + It is recommended to use a prefix that does not contain alphanumerical to avoid the possibility of duplicate usernames.""") @DefaultString(".") String usernamePrefix(); void usernamePrefix(String usernamePrefix); - @Comment + @Comment("Should spaces be replaced with '_' in bedrock usernames?") @DefaultBoolean(true) boolean replaceSpaces(); - @Comment + @Comment("The default locale for Floodgate. By default, Floodgate uses the system locale") @DefaultString("system") String defaultLocale(); @@ -97,78 +103,103 @@ public interface FloodgateConfig extends GenericPostInitializeCallback { DatabaseConfig database(); - @Comment + @Comment("Configuration for player linking") PlayerLinkConfig playerLink(); MetricsConfig metrics(); + default int version() { + return Constants.CONFIG_VERSION; + } + @Hidden @DefaultBoolean boolean debug(); - @Exclude Key key(); + @Field Key key(); - void key(Key key); + @Field void key(Key key); - @Exclude String rawUsernamePrefix(); + @Field String rawUsernamePrefix(); - void rawUsernamePrefix(String usernamePrefix); + @Field void rawUsernamePrefix(String usernamePrefix); - @ConfigSection + @ConfigSerializable interface DisconnectMessages { - @Comment + @Comment("The disconnect message Geyser users should get when connecting\n" + + "to the server with an invalid key") @DefaultString("Please connect through the official Geyser") String invalidKey(); - @Comment + @Comment("The disconnect message Geyser users should get when connecting\n" + + "to the server with the correct key but not with the correct data format") @DefaultString("Expected {} arguments, got {}. Is Geyser up-to-date?") String invalidArgumentsLength(); } - @ConfigSection + @ConfigSerializable interface DatabaseConfig { - @Comment + @Comment("Whether a database can be used (required for for example local linking)") @DefaultBoolean(true) boolean enabled(); - @Comment + @Comment("The database type you want to use.") @DefaultString("h2") String type(); } - @ConfigSection + @ConfigSerializable interface PlayerLinkConfig { - @Comment + @Comment("Whether to enable the linking system. Turning this off will prevent\n" + + "players from using the linking feature even if they are already linked.") @DefaultBoolean(true) boolean enabled(); - @Comment + @Comment("Whether to require a linked account in order to be able to join the server.") @DefaultBoolean boolean requireLink(); - @Comment + @Comment(""" + Set the following option to true when you want to host your own linking database. + -> This can work in addition to global linking. + Note that you have to enable the database in the database section as well. + """) @DefaultBoolean boolean enableOwnLinking(); - @Comment + @Comment(""" + The following two options only apply when 'enable-own-linking' is set to 'true' + + Whether to allow the use of /linkaccount and /unlinkaccount + You can also use allow specific people to use the commands using the + permissions floodgate.command.linkaccount and floodgate.command.unlinkaccount. + This is only for linking, already connected people will stay connected""") @DefaultBoolean(true) boolean allowed(); - @Comment + @Comment("The amount of seconds until a link code expires.") @DefaultNumeric(300) long linkCodeTimeout(); - @Comment + @Comment(""" + Whether to enable global linking. Global Linking is a central server where people can link their + accounts (Java and Bedrock) and join on servers that have Global Linking enabled. The goal of + Global Linking is to make linking easier by not having to link your accounts on every server. + -> Your server-specific linking database will have priority over global linking. + Global Linking should normally only be disabled when you don't have internet access or when + you have limited internet access. + """) @DefaultBoolean(true) boolean enableGlobalLinking(); } - @ConfigSection + @ConfigSerializable interface MetricsConfig { @DefaultBoolean(true) boolean enabled(); - @Placeholder - String uuid(); + default UUID uuid() { + return UUID.randomUUID(); + } } } diff --git a/core/src/main/java/org/geysermc/floodgate/core/config/ProxyFloodgateConfig.java b/core/src/main/java/org/geysermc/floodgate/core/config/ProxyFloodgateConfig.java index 333d7926..a5cc2955 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/config/ProxyFloodgateConfig.java +++ b/core/src/main/java/org/geysermc/floodgate/core/config/ProxyFloodgateConfig.java @@ -25,17 +25,19 @@ package org.geysermc.floodgate.core.config; -import org.geysermc.configutils.node.meta.Comment; -import org.geysermc.configutils.node.meta.ConfigVersion; -import org.geysermc.configutils.node.meta.Defaults.DefaultBoolean; -import org.geysermc.configutils.node.meta.Inherit; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; /** - * The Floodgate configuration used by proxy platforms, currently Velocity and Bungeecord. + * The Floodgate configuration used by proxy platforms, currently Velocity and BungeeCord. */ -@Inherit(ConfigVersion.class) +@ConfigSerializable public interface ProxyFloodgateConfig extends FloodgateConfig { - @Comment + @Comment(""" + Should the proxy send the bedrock player data to the servers it is connecting to? + This requires Floodgate to be installed on the servers. + You'll get kicked if you don't use the plugin. The default value is false because of it""") @DefaultBoolean boolean sendFloodgateData(); } diff --git a/core/src/main/java/org/geysermc/floodgate/core/util/Metrics.java b/core/src/main/java/org/geysermc/floodgate/core/util/Metrics.java index dd93e393..1d78cabe 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/util/Metrics.java +++ b/core/src/main/java/org/geysermc/floodgate/core/util/Metrics.java @@ -63,7 +63,7 @@ public final class Metrics { metricsBase = new MetricsBase( "server-implementation", - metricsConfig.uuid(), + metricsConfig.uuid().toString(), Constants.METRICS_ID, metricsConfig.enabled(), this::appendPlatformData, diff --git a/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java b/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java index fad6381a..83f21521 100644 --- a/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java +++ b/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java @@ -32,6 +32,8 @@ public final class Constants { public static final String GIT_BRANCH = "@branch@"; public static final int METRICS_ID = 14649; + public static final int CONFIG_VERSION = 3; + public static final char COLOR_CHAR = '\u00A7'; public static final boolean DEBUG_MODE = false; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c6cbe0c1..21ce7425 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ gson = "2.8.5" # core base-api = "feature-floodgate-merge-1.1.0-SNAPSHOT" -config-utils = "development-2.0-SNAPSHOT" +configurate = "4.2.0-GeyserMC-SNAPSHOT" database-utils = "1.0-SNAPSHOT" fastutil = "8.5.3" java-websocket = "1.5.2" @@ -47,8 +47,9 @@ netty-transport = { module = "io.netty:netty-transport", version.ref = "netty" } # core base-api = { module = "org.geysermc.api:base-api", version.ref = "base-api" } -config-utils = { module = "org.geysermc.configutils:core", version.ref = "config-utils" } -config-utils-ap = { module = "org.geysermc.configutils:ap", version.ref = "config-utils" } +configurate-interface-ap = { module = "org.spongepowered:configurate-extra-interface-ap", version.ref = "configurate" } +configurate-interface = { module = "org.spongepowered:configurate-extra-interface", version.ref = "configurate" } +configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } database-utils = { module = "org.geysermc.databaseutils:core", version.ref = "database-utils" } database-utils-sql = { module = "org.geysermc.databaseutils:database-sql", version.ref = "database-utils" } database-utils-mongo = { module = "org.geysermc.databaseutils:database-mongo", version.ref = "database-utils" }