diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts index 1564b7f756d..ca6c6f59cdf 100644 --- a/bootstrap/bungeecord/build.gradle.kts +++ b/bootstrap/bungeecord/build.gradle.kts @@ -12,12 +12,13 @@ dependencies { } platformRelocate("net.md_5.bungee.jni") -platformRelocate("com.fasterxml.jackson") platformRelocate("io.netty.channel.kqueue") // This is not used because relocating breaks natives, but we must include it or else we get ClassDefNotFound platformRelocate("net.kyori") platformRelocate("org.incendo") -platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated +platformRelocate("io.leangen.geantyref") // provided by cloud and Configurate, should also be relocated platformRelocate("org.yaml") // Broken as of 1.20 +platformRelocate("org.spongepowered") +platformRelocate("org.bstats") // These dependencies are already present on the platform provided(libs.bungeecord.proxy) diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/BungeeMetrics.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/BungeeMetrics.java new file mode 100644 index 00000000000..8fa3719f87f --- /dev/null +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/BungeeMetrics.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.bungeecord; + +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.config.Configuration; +import net.md_5.bungee.config.ConfigurationProvider; +import net.md_5.bungee.config.YamlConfiguration; +import org.geysermc.geyser.util.metrics.MetricsPlatform; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.UUID; + +public final class BungeeMetrics implements MetricsPlatform { + private final Configuration configuration; + + public BungeeMetrics(Plugin plugin) throws IOException { + // https://github.com/Bastian/bstats-metrics/blob/master/bungeecord/src/main/java/org/bstats/bungeecord/Metrics.java + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + //noinspection ResultOfMethodCallIgnored + bStatsFolder.mkdirs(); + File configFile = new File(bStatsFolder, "config.yml"); + if (!configFile.exists()) { + writeFile(configFile, + "# bStats (https://bStats.org) collects some basic information for plugin authors, like how", + "# many people use their plugin and their total player count. It's recommended to keep bStats", + "# enabled, but if you're not comfortable with this, you can turn this setting off. There is no", + "# performance penalty associated with having metrics enabled, and data sent to bStats is fully", + "# anonymous.", + "enabled: true", + "serverUuid: \"" + UUID.randomUUID() + "\"", + "logFailedRequests: false", + "logSentData: false", + "logResponseStatusText: false"); + } + + this.configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile); + } + + @Override + public boolean enabled() { + return configuration.getBoolean("enabled", true); + } + + @Override + public String serverUuid() { + return configuration.getString("serverUuid"); + } + + @Override + public boolean logFailedRequests() { + return configuration.getBoolean("logFailedRequests", false); + } + + @Override + public boolean logSentData() { + return configuration.getBoolean("logSentData", false); + } + + @Override + public boolean logResponseStatusText() { + return configuration.getBoolean("logResponseStatusText", false); + } + + private void writeFile(File file, String... lines) throws IOException { + try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file))) { + for (String line : lines) { + bufferedWriter.write(line); + bufferedWriter.newLine(); + } + } + } +} diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java index 7c60ba95d1a..f84702ead27 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java @@ -108,7 +108,7 @@ protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Excepti listenerInfo.isPingPassthrough(), listenerInfo.getQueryPort(), listenerInfo.isQueryEnabled(), - bootstrap.getGeyserConfig().getRemote().isUseProxyProtocol() // If Geyser is expecting HAProxy, so should the Bungee end + bootstrap.config().java().useProxyProtocol() // If Geyser is expecting HAProxy, so should the Bungee end ); // The field that stores all listeners in BungeeCord @@ -142,7 +142,7 @@ protected void initChannel(@NonNull Channel ch) throws Exception { } initChannel.invoke(channelInitializer, ch); - if (bootstrap.getGeyserConfig().isDisableCompression()) { + if (bootstrap.config().advanced().disableCompression()) { ch.pipeline().addAfter(PipelineUtils.PACKET_ENCODER, "geyser-compression-disabler", new GeyserBungeeCompressionDisabler()); } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java index 1193a52b39e..263cf5fbd96 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java @@ -67,7 +67,7 @@ public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) { try { event = future.get(100, TimeUnit.MILLISECONDS); } catch (Throwable cause) { - String address = GeyserImpl.getInstance().getConfig().isLogPlayerIpAddresses() ? inetSocketAddress.toString() : ""; + String address = GeyserImpl.getInstance().config().logPlayerIpAddresses() ? inetSocketAddress.toString() : ""; GeyserImpl.getInstance().getLogger().error("Failed to get ping information for " + address, cause); return null; } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java index 7adfd488f75..70a4d082af8 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java @@ -33,24 +33,25 @@ import net.md_5.bungee.protocol.ProtocolConstants; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.FloodgateKeyLoader; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.command.CommandSourceConverter; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.ConfigLoader; +import org.geysermc.geyser.configuration.GeyserPluginConfig; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource; import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.FileUtils; +import org.geysermc.geyser.util.metrics.MetricsPlatform; import org.incendo.cloud.CommandManager; import org.incendo.cloud.bungee.BungeeCommandManager; import org.incendo.cloud.execution.ExecutionCoordinator; -import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.net.InetSocketAddress; @@ -59,13 +60,12 @@ import java.nio.file.Paths; import java.util.Collection; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.TimeUnit; public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { private CommandRegistry commandRegistry; - private GeyserBungeeConfiguration geyserConfig; + private GeyserPluginConfig geyserConfig; private GeyserBungeeInjector geyserInjector; private final GeyserBungeeLogger geyserLogger = new GeyserBungeeLogger(getLogger()); private IGeyserPingPassthrough geyserBungeePingPassthrough; @@ -97,9 +97,7 @@ public void onGeyserInitialize() { if (!this.loadConfig()) { return; } - this.geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - this.geyser = GeyserImpl.load(PlatformType.BUNGEECORD, this); + this.geyser = GeyserImpl.load(this); this.geyserInjector = new GeyserBungeeInjector(this); // Registration of listeners occurs only once @@ -163,13 +161,11 @@ public void onGeyserEnable() { if (!loadConfig()) { return; } - this.geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); } // Force-disable query if enabled, or else Geyser won't enable for (ListenerInfo info : getProxy().getConfig().getListeners()) { - if (info.isQueryEnabled() && info.getQueryPort() == geyserConfig.getBedrock().port()) { + if (info.isQueryEnabled() && info.getQueryPort() == geyserConfig.bedrock().port()) { try { Field queryField = ListenerInfo.class.getDeclaredField("queryEnabled"); queryField.setAccessible(true); @@ -187,7 +183,7 @@ public void onGeyserEnable() { GeyserImpl.start(); - if (geyserConfig.isLegacyPingPassthrough()) { + if (!geyserConfig.integratedPingPassthrough()) { this.geyserBungeePingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } else { this.geyserBungeePingPassthrough = new GeyserBungeePingPassthrough(getProxy()); @@ -224,8 +220,13 @@ public void onDisable() { } @Override - public GeyserBungeeConfiguration getGeyserConfig() { - return geyserConfig; + public PlatformType platformType() { + return PlatformType.BUNGEECORD; + } + + @Override + public GeyserPluginConfig config() { + return this.geyserConfig; } @Override @@ -277,11 +278,29 @@ public int getServerPort() { @Override public boolean testFloodgatePluginPresent() { - if (getProxy().getPluginManager().getPlugin("floodgate") != null) { - geyserConfig.loadFloodgate(this); - return true; + return getProxy().getPluginManager().getPlugin("floodgate") != null; + } + + @Override + public Path getFloodgateKeyPath() { + Plugin floodgate = getProxy().getPluginManager().getPlugin("floodgate"); + Path geyserDataFolder = getDataFolder().toPath(); + Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; + + return FloodgateKeyLoader.getKeyPath(geyserConfig, floodgateDataFolder, geyserDataFolder, geyserLogger); + } + + @Override + public MetricsPlatform createMetricsPlatform() { + try { + return new BungeeMetrics(this); + } catch (IOException e) { + this.geyserLogger.debug("Integrated bStats support failed to load."); + if (this.config().debugMode()) { + e.printStackTrace(); + } + return null; } - return false; } private Optional findCompatibleListener() { @@ -293,17 +312,7 @@ private Optional findCompatibleListener() { @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { - try { - if (!getDataFolder().exists()) //noinspection ResultOfMethodCallIgnored - getDataFolder().mkdir(); - File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"), - "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); - this.geyserConfig = FileUtils.loadConfig(configFile, GeyserBungeeConfiguration.class); - } catch (IOException ex) { - geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); - ex.printStackTrace(); - return false; - } - return true; + this.geyserConfig = new ConfigLoader(this).createFolder().load(GeyserPluginConfig.class); + return this.geyserConfig != null; } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java index 0a89b5421c2..9c8ca2a9977 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java @@ -38,7 +38,7 @@ public final class GeyserBungeeUpdateListener implements Listener { @EventHandler public void onPlayerJoin(final PostLoginEvent event) { - if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + if (GeyserImpl.getInstance().config().notifyOnNewBedrockUpdate()) { final ProxiedPlayer player = event.getPlayer(); if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new BungeeCommandSource(player)); diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts index 2599a47b575..fc18d0e7446 100644 --- a/bootstrap/mod/fabric/build.gradle.kts +++ b/bootstrap/mod/fabric/build.gradle.kts @@ -21,7 +21,7 @@ dependencies { shadow(projects.core) { isTransitive = false } includeTransitive(projects.core) - // These are NOT transitively included, and instead shadowed + relocated. + // These are NOT transitively included, and instead shadowed (+ relocated, if not under the org.geyser namespace). // Avoids fabric complaining about non-SemVer versioning shadow(libs.protocol.connection) { isTransitive = false } shadow(libs.protocol.common) { isTransitive = false } diff --git a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java index 75da9125f3f..c4efdf9fffb 100644 --- a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java +++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.platform.fabric; +import com.google.gson.annotations.JsonAdapter; import lombok.AllArgsConstructor; import lombok.Getter; import net.fabricmc.api.EnvType; @@ -48,7 +49,7 @@ public class GeyserFabricDumpInfo extends BootstrapDumpInfo { private final String minecraftVersion; private final EnvType environmentType; - @AsteriskSerializer.Asterisk(isIp = true) + @JsonAdapter(value = AsteriskSerializer.class) private final String serverIP; private final int serverPort; private final boolean onlineMode; diff --git a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java index 4631ab4931a..345da4459e9 100644 --- a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java +++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java @@ -73,7 +73,7 @@ public boolean testFloodgatePluginPresent(@NonNull GeyserModBootstrap bootstrap) Optional floodgate = FabricLoader.getInstance().getModContainer("floodgate"); if (floodgate.isPresent()) { Path floodgateDataFolder = FabricLoader.getInstance().getConfigDir().resolve("floodgate"); - bootstrap.getGeyserConfig().loadFloodgate(bootstrap, floodgateDataFolder); + bootstrap.loadFloodgate(floodgateDataFolder); return true; } diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts index 23f16d64ef8..4ab005b4fa9 100644 --- a/bootstrap/mod/neoforge/build.gradle.kts +++ b/bootstrap/mod/neoforge/build.gradle.kts @@ -13,9 +13,6 @@ architectury { provided("org.cloudburstmc.math", "api") provided("com.google.errorprone", "error_prone_annotations") -// Jackson shipped by Minecraft is too old, so we shade & relocate our newer version -relocate("com.fasterxml.jackson") - val includeTransitive: Configuration = configurations.getByName("includeTransitive") dependencies { @@ -34,12 +31,6 @@ dependencies { } shadow(projects.core) { isTransitive = false } - // Minecraft (1.21.2+) includes jackson. But an old version! - shadow(libs.jackson.core) { isTransitive = false } - shadow(libs.jackson.databind) { isTransitive = false } - shadow(libs.jackson.dataformat.yaml) { isTransitive = false } - shadow(libs.jackson.annotations) { isTransitive = false } - // Let's shade in our own api shadow(projects.api) { isTransitive = false } @@ -65,11 +56,6 @@ tasks { remapModrinthJar { archiveBaseName.set("geyser-neoforge") } - - shadowJar { - // Without this, jackson's service files are not relocated - mergeServiceFiles() - } } modrinth { diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java index 623f68d3a1f..c8104f2d487 100644 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.platform.neoforge; +import com.google.gson.annotations.JsonAdapter; import lombok.AllArgsConstructor; import lombok.Getter; import net.minecraft.server.MinecraftServer; @@ -47,7 +48,7 @@ public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo { private final String minecraftVersion; private final Dist dist; - @AsteriskSerializer.Asterisk(isIp = true) + @JsonAdapter(value = AsteriskSerializer.class) private final String serverIP; private final int serverPort; private final boolean onlineMode; @@ -81,4 +82,4 @@ public static class ModInfo { public String version; public String url; } -} \ No newline at end of file +} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java index 41562baf326..c314009fb54 100644 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java @@ -73,7 +73,7 @@ public GeyserNeoForgePlatform(ModContainer container) { public boolean testFloodgatePluginPresent(@NonNull GeyserModBootstrap bootstrap) { if (ModList.get().isLoaded("floodgate")) { Path floodgateDataFolder = FMLPaths.CONFIGDIR.get().resolve("floodgate"); - bootstrap.getGeyserConfig().loadFloodgate(bootstrap, floodgateDataFolder); + bootstrap.loadFloodgate(floodgateDataFolder); return true; } return false; diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java index 69d6dc9a45d..879ecc76f5f 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java @@ -31,11 +31,14 @@ import net.minecraft.server.MinecraftServer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.FloodgateKeyLoader; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.CommandRegistry; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.ConfigLoader; +import org.geysermc.geyser.configuration.GeyserPluginConfig; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; @@ -43,14 +46,10 @@ import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform; import org.geysermc.geyser.platform.mod.world.GeyserModWorldManager; import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.FileUtils; -import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.net.SocketAddress; import java.nio.file.Path; -import java.util.UUID; @RequiredArgsConstructor public abstract class GeyserModBootstrap implements GeyserBootstrap { @@ -69,7 +68,7 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { @Setter private CommandRegistry commandRegistry; - private GeyserModConfiguration geyserConfig; + private GeyserPluginConfig geyserConfig; private GeyserModInjector geyserInjector; private final GeyserModLogger geyserLogger = new GeyserModLogger(); private IGeyserPingPassthrough geyserPingPassthrough; @@ -83,9 +82,7 @@ public void onGeyserInitialize() { if (!loadConfig()) { return; } - this.geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - this.geyser = GeyserImpl.load(this.platform.platformType(), this); + this.geyser = GeyserImpl.load(this); } public void onGeyserEnable() { @@ -98,13 +95,11 @@ public void onGeyserEnable() { if (!loadConfig()) { return; } - this.geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); } GeyserImpl.start(); - if (geyserConfig.isLegacyPingPassthrough()) { + if (!geyserConfig.integratedPingPassthrough()) { this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } else { this.geyserPingPassthrough = new ModPingPassthrough(server, geyserLogger); @@ -145,7 +140,12 @@ public void onGeyserShutdown() { } @Override - public GeyserModConfiguration getGeyserConfig() { + public PlatformType platformType() { + return this.platform.platformType(); + } + + @Override + public GeyserPluginConfig config() { return geyserConfig; } @@ -199,12 +199,7 @@ public SocketAddress getSocketAddress() { @Override public int getServerPort() { - if (isServer()) { - return ((GeyserServerPortGetter) server).geyser$getServerPort(); - } else { - // Set in the IntegratedServerMixin - return geyserConfig.getRemote().port(); - } + return ((GeyserServerPortGetter) server).geyser$getServerPort(); } public abstract boolean isServer(); @@ -214,6 +209,17 @@ public boolean testFloodgatePluginPresent() { return this.platform.testFloodgatePluginPresent(this); } + private Path floodgateKeyPath; + + public void loadFloodgate(Path floodgateDataFolder) { + floodgateKeyPath = FloodgateKeyLoader.getKeyPath(geyserConfig, floodgateDataFolder, dataFolder, geyserLogger); + } + + @Override + public Path getFloodgateKeyPath() { + return floodgateKeyPath; + } + @Nullable @Override public InputStream getResourceOrNull(String resource) { @@ -222,20 +228,7 @@ public InputStream getResourceOrNull(String resource) { @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { - try { - if (!dataFolder.toFile().exists()) { - //noinspection ResultOfMethodCallIgnored - dataFolder.toFile().mkdir(); - } - - File configFile = FileUtils.fileOrCopiedFromResource(dataFolder.resolve("config.yml").toFile(), "config.yml", - (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); - this.geyserConfig = FileUtils.loadConfig(configFile, GeyserModConfiguration.class); - return true; - } catch (IOException ex) { - geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); - ex.printStackTrace(); - return false; - } + this.geyserConfig = new ConfigLoader(this).createFolder().load(GeyserPluginConfig.class); + return this.geyserConfig != null; } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java index 624eccb3f24..4bfc7464a3a 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java @@ -96,7 +96,7 @@ protected void initChannel(@NonNull Channel ch) throws Exception { int index = ch.pipeline().names().indexOf("encoder"); String baseName = index != -1 ? "encoder" : "outbound_config"; - if (bootstrap.getGeyserConfig().isDisableCompression()) { + if (bootstrap.config().advanced().disableCompression()) { ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserModCompressionDisabler()); } } @@ -125,7 +125,7 @@ private ChannelInitializer getChildHandler(GeyserBootstrap bootstrap, C childHandler = (ChannelInitializer) childHandlerField.get(handler); break; } catch (Exception e) { - if (bootstrap.getGeyserConfig().isDebugMode()) { + if (bootstrap.config().debugMode()) { bootstrap.getGeyserLogger().debug("The handler " + name + " isn't a ChannelInitializer. THIS ERROR IS SAFE TO IGNORE!"); e.printStackTrace(); } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java index ece2f730a1a..8f218105f7f 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java @@ -56,14 +56,13 @@ private void onOpenToLan(GameType gameType, boolean cheatsAllowed, int port, Cal // If the LAN is opened, starts Geyser. GeyserModBootstrap instance = GeyserModBootstrap.getInstance(); instance.setServer((MinecraftServer) (Object) this); - instance.getGeyserConfig().getRemote().setPort(port); instance.onGeyserEnable(); // Ensure player locale has been loaded, in case it's different from Java system language GeyserLocale.loadGeyserLocale(this.minecraft.options.languageCode); // Give indication that Geyser is loaded Objects.requireNonNull(this.minecraft.player); - this.minecraft.player.displayClientMessage(Component.literal(GeyserLocale.getPlayerLocaleString("geyser.core.start", - this.minecraft.options.languageCode, "localhost", String.valueOf(GeyserImpl.getInstance().bedrockListener().port()))), false); + this.minecraft.player.displayClientMessage(Component.literal(GeyserLocale.getPlayerLocaleString("geyser.core.start.ip_suppressed", + this.minecraft.options.languageCode, String.valueOf(GeyserImpl.getInstance().bedrockListener().port()))), false); } } diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index feabfdd7af6..29115f7d90e 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -30,17 +30,24 @@ dependencies { compileOnly(libs.folia.api) compileOnlyApi(libs.viaversion) + + // For 1.16.5/1.17.1 + implementation(libs.gson.record.factory) { + isTransitive = false + } } platformRelocate("it.unimi.dsi.fastutil") -platformRelocate("com.fasterxml.jackson") // Relocate net.kyori but exclude the component logger platformRelocate("net.kyori", "net.kyori.adventure.text.logger.slf4j.ComponentLogger") platformRelocate("org.objectweb.asm") platformRelocate("me.lucko.commodore") platformRelocate("org.incendo") -platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated +platformRelocate("io.leangen.geantyref") // provided by cloud and Configurate, should also be relocated platformRelocate("org.yaml") // Broken as of 1.20 +platformRelocate("org.spongepowered") +platformRelocate("marcono1234.gson") +platformRelocate("org.bstats") // These dependencies are already present on the platform provided(libs.viaversion) diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotConfiguration.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotConfiguration.java deleted file mode 100644 index 3320ffa6595..00000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.platform.spigot; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; -import org.geysermc.geyser.FloodgateKeyLoader; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; - -import java.nio.file.Path; - -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public final class GeyserSpigotConfiguration extends GeyserJacksonConfiguration { - @JsonIgnore - private Path floodgateKeyPath; - - public void loadFloodgate(GeyserSpigotPlugin plugin) { - Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); - Path geyserDataFolder = plugin.getDataFolder().toPath(); - Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; - - floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgateDataFolder, geyserDataFolder, plugin.getGeyserLogger()); - } -} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java index 329663709fc..6e063f41575 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.platform.spigot; +import com.google.gson.annotations.JsonAdapter; import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; @@ -42,7 +43,7 @@ public class GeyserSpigotDumpInfo extends BootstrapDumpInfo { private final String platformAPIVersion; private final boolean onlineMode; - @AsteriskSerializer.Asterisk(isIp = true) + @JsonAdapter(value = AsteriskSerializer.class) private final String serverIP; private final int serverPort; private final List plugins; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java index 5dcfbd0f8b3..c9500031a89 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java @@ -25,10 +25,13 @@ package org.geysermc.geyser.platform.spigot; -import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; import com.viaversion.viaversion.bukkit.handlers.BukkitChannelInitializer; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.DefaultEventLoopGroup; import io.netty.channel.local.LocalAddress; import io.netty.util.concurrent.DefaultThreadFactory; import org.bukkit.Bukkit; @@ -37,6 +40,7 @@ import org.geysermc.geyser.network.netty.GeyserInjector; import org.geysermc.geyser.network.netty.LocalServerChannelWrapper; import org.geysermc.geyser.network.netty.LocalSession; +import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -122,7 +126,7 @@ protected void initChannel(@NonNull Channel ch) throws Exception { int index = ch.pipeline().names().indexOf("encoder"); String baseName = index != -1 ? "encoder" : "outbound_config"; - if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserSpigotCompressionDisabler.ENABLED) { + if (bootstrap.config().advanced().disableCompression() && GeyserSpigotCompressionDisabler.ENABLED) { ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserSpigotCompressionDisabler()); } } @@ -157,7 +161,7 @@ private ChannelInitializer getChildHandler(GeyserBootstrap bootstrap, C } break; } catch (Exception e) { - if (bootstrap.getGeyserConfig().isDebugMode()) { + if (bootstrap.config().debugMode()) { bootstrap.getGeyserLogger().debug("The handler " + name + " isn't a ChannelInitializer. THIS ERROR IS SAFE TO IGNORE!"); e.printStackTrace(); } @@ -176,8 +180,8 @@ private ChannelInitializer getChildHandler(GeyserBootstrap bootstrap, C */ private void workAroundWeirdBug(GeyserBootstrap bootstrap) { MinecraftProtocol protocol = new MinecraftProtocol(); - LocalSession session = new LocalSession(bootstrap.getGeyserConfig().getRemote().address(), - bootstrap.getGeyserConfig().getRemote().port(), this.serverSocketAddress, + LocalSession session = new LocalSession(bootstrap.config().java().address(), + bootstrap.config().java().port(), this.serverSocketAddress, InetAddress.getLoopbackAddress().getHostAddress(), protocol, protocol.createHelper()); session.connect(); session.disconnect(""); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java index a2d5c992bed..74364f57e16 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java @@ -38,9 +38,11 @@ import org.bukkit.event.server.ServerLoadEvent; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.FloodgateKeyLoader; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.adapters.paper.PaperAdapters; @@ -50,7 +52,8 @@ import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.command.CommandSourceConverter; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.ConfigLoader; +import org.geysermc.geyser.configuration.GeyserPluginConfig; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.GameProtocol; @@ -64,23 +67,20 @@ import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotNativeWorldManager; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager; import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.FileUtils; +import org.geysermc.geyser.util.metrics.MetricsPlatform; import org.incendo.cloud.bukkit.BukkitCommandManager; import org.incendo.cloud.execution.ExecutionCoordinator; import org.incendo.cloud.paper.LegacyPaperCommandManager; -import java.io.File; -import java.io.IOException; import java.net.SocketAddress; import java.nio.file.Path; import java.util.List; import java.util.Objects; -import java.util.UUID; public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { private CommandRegistry commandRegistry; - private GeyserSpigotConfiguration geyserConfig; + private GeyserPluginConfig geyserConfig; private GeyserSpigotInjector geyserInjector; private final GeyserSpigotLogger geyserLogger = GeyserPaperLogger.supported() ? new GeyserPaperLogger(this, getLogger()) : new GeyserSpigotLogger(getLogger()); @@ -164,13 +164,11 @@ public void onGeyserInitialize() { if (!loadConfig()) { return; } - this.geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); // Turn "(MC: 1.16.4)" into 1.16.4. this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0]; - this.geyser = GeyserImpl.load(PlatformType.SPIGOT, this); + this.geyser = GeyserImpl.load(this); } @Override @@ -229,13 +227,11 @@ public void onGeyserEnable() { if (!loadConfig()) { return; } - this.geyserLogger.setDebug(this.geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); } GeyserImpl.start(); - if (geyserConfig.isLegacyPingPassthrough()) { + if (!geyserConfig.integratedPingPassthrough()) { this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } else { if (ReflectedNames.checkPaperPingEvent()) { @@ -289,7 +285,7 @@ public void onGeyserEnable() { } geyserLogger.debug("Using world manager of type: " + this.geyserWorldManager.getClass().getSimpleName()); } catch (Throwable e) { - if (geyserConfig.isDebugMode()) { + if (geyserConfig.debugMode()) { geyserLogger.debug("Error while attempting to find NMS adapter. Most likely, this can be safely ignored. :)"); e.printStackTrace(); } @@ -363,8 +359,13 @@ public void onDisable() { } @Override - public GeyserSpigotConfiguration getGeyserConfig() { - return geyserConfig; + public PlatformType platformType() { + return PlatformType.SPIGOT; + } + + @Override + public GeyserPluginConfig config() { + return this.geyserConfig; } @Override @@ -450,31 +451,30 @@ public int getServerPort() { @Override public boolean testFloodgatePluginPresent() { - if (Bukkit.getPluginManager().getPlugin("floodgate") != null) { - geyserConfig.loadFloodgate(this); - return true; - } - return false; + return Bukkit.getPluginManager().getPlugin("floodgate") != null; + } + + @Override + public Path getFloodgateKeyPath() { + Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); + Path geyserDataFolder = getDataFolder().toPath(); + Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; + + return FloodgateKeyLoader.getKeyPath(geyserConfig, floodgateDataFolder, geyserDataFolder, geyserLogger); + } + + @Override + public MetricsPlatform createMetricsPlatform() { + return new SpigotMetrics(this); } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { - // This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed - try { - if (!getDataFolder().exists()) { - //noinspection ResultOfMethodCallIgnored - getDataFolder().mkdir(); - } - File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"), "config.yml", - (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); - this.geyserConfig = FileUtils.loadConfig(configFile, GeyserSpigotConfiguration.class); - } catch (IOException ex) { - geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); - ex.printStackTrace(); + this.geyserConfig = new ConfigLoader(this).createFolder().load(GeyserPluginConfig.class); + if (this.geyserConfig == null) { Bukkit.getPluginManager().disablePlugin(this); return false; } - return true; } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java index 8a8a43460b6..a4eee8a2061 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java @@ -38,7 +38,7 @@ public final class GeyserSpigotUpdateListener implements Listener { @EventHandler public void onPlayerJoin(final PlayerJoinEvent event) { - if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + if (GeyserImpl.getInstance().config().notifyOnNewBedrockUpdate()) { final Player player = event.getPlayer(); if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new SpigotCommandSource(player)); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/SpigotMetrics.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/SpigotMetrics.java new file mode 100644 index 00000000000..796ae4b881e --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/SpigotMetrics.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.platform.spigot; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.geysermc.geyser.util.metrics.MetricsPlatform; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public final class SpigotMetrics implements MetricsPlatform { + private final YamlConfiguration config; + + public SpigotMetrics(Plugin plugin) { + // https://github.com/Bastian/bstats-metrics/blob/master/bukkit/src/main/java/org/bstats/bukkit/Metrics.java + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + config = YamlConfiguration.loadConfiguration(configFile); + + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true); + config.addDefault("serverUuid", UUID.randomUUID().toString()); + config.addDefault("logFailedRequests", false); + config.addDefault("logSentData", false); + config.addDefault("logResponseStatusText", false); + + // Inform the server owners about bStats + config.options().header( + "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + + "many people use their plugin and their total player count. It's recommended to keep bStats\n" + + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + + "anonymous." + ).copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { } + } + } + + @Override + public boolean enabled() { + return config.getBoolean("enabled", true); + } + + @Override + public String serverUuid() { + return config.getString("serverUuid"); + } + + @Override + public boolean logFailedRequests() { + return config.getBoolean("logFailedRequests", false); + } + + @Override + public boolean logSentData() { + return config.getBoolean("logSentData", false); + } + + @Override + public boolean logResponseStatusText() { + return config.getBoolean("logResponseStatusText", false); + } +} diff --git a/bootstrap/standalone/build.gradle.kts b/bootstrap/standalone/build.gradle.kts index b210693c1f3..c6ced739ef6 100644 --- a/bootstrap/standalone/build.gradle.kts +++ b/bootstrap/standalone/build.gradle.kts @@ -19,6 +19,8 @@ dependencies { implementation(libs.bundles.jline) implementation(libs.bundles.log4j) + + implementation(libs.gson.runtime) } application { diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index 87fbbf0aad5..9a55d5fff2b 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -25,11 +25,6 @@ package org.geysermc.geyser.platform.standalone; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.introspect.AnnotatedField; -import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.netty.util.ResourceLeakDetector; import lombok.Getter; import net.minecrell.terminalconsole.TerminalConsoleAppender; @@ -44,35 +39,32 @@ import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager; -import org.geysermc.geyser.configuration.GeyserConfiguration; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; +import org.geysermc.geyser.configuration.ConfigLoader; +import org.geysermc.geyser.configuration.GeyserConfig; +import org.geysermc.geyser.configuration.GeyserRemoteConfig; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import org.geysermc.geyser.platform.standalone.gui.GeyserStandaloneGUI; import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.LoopbackUtil; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.NodePath; +import org.spongepowered.configurate.serialize.SerializationException; import java.io.File; -import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Path; import java.nio.file.Paths; import java.text.MessageFormat; import java.util.HashMap; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; public class GeyserStandaloneBootstrap implements GeyserBootstrap { private StandaloneCloudCommandManager cloud; private CommandRegistry commandRegistry; - private GeyserStandaloneConfiguration geyserConfig; + private GeyserConfig geyserConfig; private final GeyserStandaloneLogger geyserLogger = new GeyserStandaloneLogger(); private IGeyserPingPassthrough geyserPingPassthrough; private GeyserStandaloneGUI gui; @@ -83,9 +75,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { private GeyserImpl geyser; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private static final Map argsConfigKeys = new HashMap<>(); + private static final Map argsConfigKeys = new HashMap<>(); public static void main(String[] args) { if (System.getProperty("io.netty.leakDetection.level") == null) { @@ -99,8 +89,6 @@ public static void main(String[] args) { GeyserLocale.init(bootstrap); - List availableProperties = getPOJOForClass(GeyserJacksonConfiguration.class); - for (int i = 0; i < args.length; i++) { // By default, standalone Geyser will check if it should open the GUI based on if the GUI is null // Optionally, you can force the use of a GUI or no GUI by specifying args @@ -132,34 +120,8 @@ public static void main(String[] args) { // Split the argument by an = String[] argParts = arg.substring(2).split("="); if (argParts.length == 2) { - // Split the config key by . to allow for nested options - String[] configKeyParts = argParts[0].split("\\."); - - // Loop the possible config options to check the passed key is valid - boolean found = false; - for (BeanPropertyDefinition property : availableProperties) { - if (configKeyParts[0].equals(property.getName())) { - if (configKeyParts.length > 1) { - // Loop sub-section options to check the passed key is valid - for (BeanPropertyDefinition subProperty : getPOJOForClass(property.getRawPrimaryType())) { - if (configKeyParts[1].equals(subProperty.getName())) { - found = true; - break; - } - } - } else { - found = true; - } - - break; - } - } - - // Add the found key to the stored list for later usage - if (found) { - argsConfigKeys.put(argParts[0], argParts[1]); - break; - } + argsConfigKeys.put(NodePath.of(argParts[0].split("\\.")), argParts[1]); + break; } } System.err.println(GeyserLocale.getLocaleStringLog("geyser.bootstrap.args.unrecognised", arg)); @@ -196,19 +158,10 @@ public void onGeyserInitialize() { @Override public void onGeyserEnable() { - try { - File configFile = FileUtils.fileOrCopiedFromResource(new File(configFilename), "config.yml", - (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); - geyserConfig = FileUtils.loadConfig(configFile, GeyserStandaloneConfiguration.class); - - handleArgsConfigOptions(); - - if (this.geyserConfig.getRemote().address().equalsIgnoreCase("auto")) { - geyserConfig.setAutoconfiguredRemote(true); // Doesn't really need to be set but /shrug - geyserConfig.getRemote().setAddress("127.0.0.1"); - } - } catch (IOException ex) { - geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); + this.geyserConfig = new ConfigLoader(this) + .transformer(this::handleArgsConfigOptions) + .load(GeyserRemoteConfig.class); + if (this.geyserConfig == null) { if (gui == null) { System.exit(1); } else { @@ -216,13 +169,11 @@ public void onGeyserEnable() { return; } } - geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); // Allow libraries like Protocol to have their debug information passthrough - log4jLogger.get().setLevel(geyserConfig.isDebugMode() ? Level.DEBUG : Level.INFO); + log4jLogger.get().setLevel(geyserConfig.debugMode() ? Level.DEBUG : Level.INFO); - geyser = GeyserImpl.load(PlatformType.STANDALONE, this); + geyser = GeyserImpl.load(this); boolean reloading = geyser.isReloading(); if (!reloading) { @@ -278,8 +229,13 @@ public void onGeyserShutdown() { } @Override - public GeyserConfiguration getGeyserConfig() { - return geyserConfig; + public PlatformType platformType() { + return PlatformType.STANDALONE; + } + + @Override + public GeyserConfig config() { + return this.geyserConfig; } @Override @@ -330,100 +286,47 @@ public boolean testFloodgatePluginPresent() { return false; } - /** - * Get the {@link BeanPropertyDefinition}s for the given class - * - * @param clazz The class to get the definitions for - * @return A list of {@link BeanPropertyDefinition} for the given class - */ - public static List getPOJOForClass(Class clazz) { - JavaType javaType = OBJECT_MAPPER.getTypeFactory().constructType(clazz); - - // Introspect the given type - BeanDescription beanDescription = OBJECT_MAPPER.getSerializationConfig().introspect(javaType); - - // Find properties - List properties = beanDescription.findProperties(); - - // Get the ignored properties - Set ignoredProperties = OBJECT_MAPPER.getSerializationConfig().getAnnotationIntrospector() - .findPropertyIgnoralByName(OBJECT_MAPPER.getSerializationConfig() ,beanDescription.getClassInfo()).getIgnored(); - - // Filter properties removing the ignored ones - return properties.stream() - .filter(property -> !ignoredProperties.contains(property.getName())) - .collect(Collectors.toList()); + @Override + public Path getFloodgateKeyPath() { + return Path.of(geyserConfig.advanced().floodgateKeyFile()); } /** * Set a POJO property value on an object * - * @param property The {@link BeanPropertyDefinition} to set - * @param parentObject The object to alter * @param value The new value of the property */ - @SuppressWarnings({"unchecked", "rawtypes"}) // Required for enum usage - private static void setConfigOption(BeanPropertyDefinition property, Object parentObject, Object value) { + private static void setConfigOption(CommentedConfigurationNode node, Object value) throws SerializationException { Object parsedValue = value; // Change the values type if needed - if (int.class.equals(property.getRawPrimaryType())) { + Class clazz = node.raw().getClass(); + if (Integer.class == clazz) { parsedValue = Integer.valueOf((String) parsedValue); - } else if (boolean.class.equals(property.getRawPrimaryType())) { + } else if (Boolean.class == clazz) { parsedValue = Boolean.valueOf((String) parsedValue); - } else if (Enum.class.isAssignableFrom(property.getRawPrimaryType())) { - parsedValue = Enum.valueOf((Class) property.getRawPrimaryType(), ((String) parsedValue).toUpperCase(Locale.ROOT)); } - // Force the value to be set - AnnotatedField field = property.getField(); - field.fixAccess(true); - field.setValue(parentObject, parsedValue); + node.set(parsedValue); } /** - * Update the loaded {@link GeyserStandaloneConfiguration} with any values passed in the command line arguments + * Update the loaded config with any values passed in the command line arguments */ - private void handleArgsConfigOptions() { - // Get the available properties from the class - List availableProperties = getPOJOForClass(GeyserJacksonConfiguration.class); - - for (Map.Entry configKey : argsConfigKeys.entrySet()) { - String[] configKeyParts = configKey.getKey().split("\\."); - - // Loop over the properties looking for any matches against the stored one from the argument - for (BeanPropertyDefinition property : availableProperties) { - if (configKeyParts[0].equals(property.getName())) { - if (configKeyParts.length > 1) { - // Loop through the sub property if the first part matches - for (BeanPropertyDefinition subProperty : getPOJOForClass(property.getRawPrimaryType())) { - if (configKeyParts[1].equals(subProperty.getName())) { - geyserLogger.info(GeyserLocale.getLocaleStringLog("geyser.bootstrap.args.set_config_option", configKey.getKey(), configKey.getValue())); - - // Set the sub property value on the config - try { - Object subConfig = property.getGetter().callOn(geyserConfig); - setConfigOption(subProperty, subConfig, configKey.getValue()); - } catch (Exception e) { - geyserLogger.error("Failed to set config option: " + property.getFullName()); - } - - break; - } - } - } else { - geyserLogger.info(GeyserLocale.getLocaleStringLog("geyser.bootstrap.args.set_config_option", configKey.getKey(), configKey.getValue())); - - // Set the property value on the config - try { - setConfigOption(property, geyserConfig, configKey.getValue()); - } catch (Exception e) { - geyserLogger.error("Failed to set config option: " + property.getFullName()); - } - } + private void handleArgsConfigOptions(CommentedConfigurationNode node) { + for (Map.Entry configKey : argsConfigKeys.entrySet()) { + NodePath path = configKey.getKey(); + CommentedConfigurationNode subNode = node.node(path); + if (subNode.virtual()) { + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.args.unrecognised", path)); + continue; + } - break; - } + try { + setConfigOption(subNode, configKey.getValue()); + geyserLogger.info(GeyserLocale.getLocaleStringLog("geyser.bootstrap.args.set_config_option", configKey.getKey(), configKey.getValue())); + } catch (SerializationException e) { + geyserLogger.error("Failed to set config option: " + path); } } } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java index b614a7b2375..5636c0ebfc7 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java @@ -29,6 +29,7 @@ import net.minecrell.terminalconsole.SimpleTerminalConsole; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.command.GeyserCommandSource; @@ -100,7 +101,12 @@ public void info(String message) { @Override public void debug(String message) { - log.debug(ChatColor.GRAY + message); + log.debug(ChatColor.GRAY + "{}", message); + } + + @Override + public void debug(@Nullable Object object) { + log.debug("{}", object); } @Override diff --git a/bootstrap/velocity/build.gradle.kts b/bootstrap/velocity/build.gradle.kts index 05035e27145..94c609d7883 100644 --- a/bootstrap/velocity/build.gradle.kts +++ b/bootstrap/velocity/build.gradle.kts @@ -11,12 +11,13 @@ dependencies { api(libs.cloud.velocity) } -platformRelocate("com.fasterxml.jackson") platformRelocate("it.unimi.dsi.fastutil") platformRelocate("net.kyori.adventure.text.serializer.gson.legacyimpl") platformRelocate("org.yaml") +platformRelocate("org.spongepowered") +platformRelocate("org.bstats") platformRelocate("org.incendo") -platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated +platformRelocate("io.leangen.geantyref") // provided by cloud and Configurate, should also be relocated exclude("com.google.*:*") diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityDumpInfo.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityDumpInfo.java index 45eb7abb9a5..6bc309f2990 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityDumpInfo.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityDumpInfo.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.platform.velocity; +import com.google.gson.annotations.JsonAdapter; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.ProxyServer; import lombok.Getter; @@ -42,7 +43,7 @@ public class GeyserVelocityDumpInfo extends BootstrapDumpInfo { private final String platformVendor; private final boolean onlineMode; - @AsteriskSerializer.Asterisk(isIp = true) + @JsonAdapter(value = AsteriskSerializer.class) private final String serverIP; private final int serverPort; private final List plugins; diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java index 68a9eb40b89..ef56b1bfa10 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java @@ -27,7 +27,12 @@ import com.velocitypowered.api.proxy.ProxyServer; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.local.LocalAddress; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; @@ -80,7 +85,7 @@ protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Excepti protected void initChannel(@NonNull Channel ch) throws Exception { initChannel.invoke(channelInitializer, ch); - if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) { + if (bootstrap.config().advanced().disableCompression() && GeyserVelocityCompressionDisabler.ENABLED) { ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler", new GeyserVelocityCompressionDisabler()); } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java index 8fa47f56920..017087d9077 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java @@ -39,31 +39,32 @@ import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.FloodgateKeyLoader; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.command.CommandSourceConverter; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.ConfigLoader; +import org.geysermc.geyser.configuration.GeyserPluginConfig; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource; import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.FileUtils; +import org.geysermc.geyser.util.metrics.MetricsPlatform; import org.incendo.cloud.CommandManager; import org.incendo.cloud.execution.ExecutionCoordinator; import org.incendo.cloud.velocity.VelocityCommandManager; import org.slf4j.Logger; -import java.io.File; import java.io.IOException; import java.net.SocketAddress; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.UUID; +import java.util.Optional; @Plugin(id = "geyser", name = GeyserImpl.NAME + "-Velocity", version = GeyserImpl.VERSION, url = "https://geysermc.org", authors = "GeyserMC") public class GeyserVelocityPlugin implements GeyserBootstrap { @@ -71,7 +72,7 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { private final ProxyServer proxyServer; private final PluginContainer container; private final GeyserVelocityLogger geyserLogger; - private GeyserVelocityConfiguration geyserConfig; + private GeyserPluginConfig geyserConfig; private GeyserVelocityInjector geyserInjector; private IGeyserPingPassthrough geyserPingPassthrough; private CommandRegistry commandRegistry; @@ -104,10 +105,8 @@ public void onGeyserInitialize() { if (!loadConfig()) { return; } - this.geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - this.geyser = GeyserImpl.load(PlatformType.VELOCITY, this); + this.geyser = GeyserImpl.load(this); this.geyserInjector = new GeyserVelocityInjector(proxyServer); // We need to register commands here, rather than in onGeyserEnable which is invoked during the appropriate ListenerBoundEvent. @@ -137,13 +136,11 @@ public void onGeyserEnable() { if (!loadConfig()) { return; } - this.geyserLogger.setDebug(geyserConfig.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); } GeyserImpl.start(); - if (geyserConfig.isLegacyPingPassthrough()) { + if (!geyserConfig.integratedPingPassthrough()) { this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } else { this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer); @@ -173,7 +170,12 @@ public void onGeyserShutdown() { } @Override - public GeyserVelocityConfiguration getGeyserConfig() { + public PlatformType platformType() { + return PlatformType.VELOCITY; + } + + @Override + public GeyserPluginConfig config() { return geyserConfig; } @@ -240,27 +242,32 @@ public int getServerPort() { @Override public boolean testFloodgatePluginPresent() { var floodgate = proxyServer.getPluginManager().getPlugin("floodgate"); - if (floodgate.isPresent()) { - geyserConfig.loadFloodgate(this, proxyServer, configFolder.toFile()); - return true; + return floodgate.isPresent(); + } + + @Override + public Path getFloodgateKeyPath() { + Optional floodgate = proxyServer.getPluginManager().getPlugin("floodgate"); + Path floodgateDataPath = floodgate.isPresent() ? Paths.get("plugins/floodgate/") : null; + return FloodgateKeyLoader.getKeyPath(geyserConfig, floodgateDataPath, configFolder, geyserLogger); + } + + @Override + public MetricsPlatform createMetricsPlatform() { + try { + return new VelocityMetrics(this.configFolder); + } catch (IOException e) { + this.geyserLogger.debug("Integrated bStats support failed to load."); + if (this.config().debugMode()) { + e.printStackTrace(); + } + return null; } - return false; } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { - try { - if (!configFolder.toFile().exists()) - //noinspection ResultOfMethodCallIgnored - configFolder.toFile().mkdirs(); - File configFile = FileUtils.fileOrCopiedFromResource(configFolder.resolve("config.yml").toFile(), - "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); - this.geyserConfig = FileUtils.loadConfig(configFile, GeyserVelocityConfiguration.class); - } catch (IOException ex) { - geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); - ex.printStackTrace(); - return false; - } - return true; + this.geyserConfig = new ConfigLoader(this).createFolder().load(GeyserPluginConfig.class); + return this.geyserConfig != null; } } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java index c1c88b70d44..de3d7fa8aed 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java @@ -37,7 +37,7 @@ public final class GeyserVelocityUpdateListener { @Subscribe public void onPlayerJoin(PostLoginEvent event) { - if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + if (GeyserImpl.getInstance().config().notifyOnNewBedrockUpdate()) { final Player player = event.getPlayer(); if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new VelocityCommandSource(player)); diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityConfiguration.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/VelocityMetrics.java similarity index 50% rename from bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityConfiguration.java rename to bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/VelocityMetrics.java index 4c8ea53cc89..5939c6678fd 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityConfiguration.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/VelocityMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,28 +25,45 @@ package org.geysermc.geyser.platform.velocity; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.velocitypowered.api.plugin.PluginContainer; -import com.velocitypowered.api.proxy.ProxyServer; -import lombok.Getter; -import org.geysermc.geyser.FloodgateKeyLoader; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; +import org.bstats.config.MetricsConfig; +import org.geysermc.geyser.util.metrics.MetricsPlatform; import java.io.File; +import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Optional; - -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public final class GeyserVelocityConfiguration extends GeyserJacksonConfiguration { - @JsonIgnore - private Path floodgateKeyPath; - - public void loadFloodgate(GeyserVelocityPlugin plugin, ProxyServer proxyServer, File dataFolder) { - Optional floodgate = proxyServer.getPluginManager().getPlugin("floodgate"); - Path floodgateDataPath = floodgate.isPresent() ? Paths.get("plugins/floodgate/") : null; - floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgateDataPath, dataFolder.toPath(), plugin.getGeyserLogger()); + +public final class VelocityMetrics implements MetricsPlatform { + private final MetricsConfig config; + + public VelocityMetrics(Path dataDirectory) throws IOException { + // https://github.com/Bastian/bstats-metrics/blob/master/velocity/src/main/java/org/bstats/velocity/Metrics.java + File configFile = dataDirectory.getParent().resolve("bStats").resolve("config.txt").toFile(); + this.config = new MetricsConfig(configFile, true); + // No logger message is implemented as Velocity should print its own before we do. + } + + @Override + public boolean enabled() { + return config.isEnabled(); + } + + @Override + public String serverUuid() { + return config.getServerUUID(); + } + + @Override + public boolean logFailedRequests() { + return config.isLogErrorsEnabled(); + } + + @Override + public boolean logSentData() { + return config.isLogSentDataEnabled(); + } + + @Override + public boolean logResponseStatusText() { + return config.isLogResponseStatusTextEnabled(); } } diff --git a/bootstrap/viaproxy/build.gradle.kts b/bootstrap/viaproxy/build.gradle.kts index c13862a27f2..1a90ef9785e 100644 --- a/bootstrap/viaproxy/build.gradle.kts +++ b/bootstrap/viaproxy/build.gradle.kts @@ -12,8 +12,9 @@ platformRelocate("net.kyori") platformRelocate("org.yaml") platformRelocate("it.unimi.dsi.fastutil") platformRelocate("org.cloudburstmc.netty") +platformRelocate("org.bstats") platformRelocate("org.incendo") -platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated +platformRelocate("io.leangen.geantyref") // provided by cloud and Configurate, should also be relocated // These dependencies are already present on the platform provided(libs.viaproxy) diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java deleted file mode 100644 index afc46fa6aac..00000000000 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ -package org.geysermc.geyser.platform.viaproxy; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import net.raphimc.vialegacy.api.LegacyProtocolVersion; -import net.raphimc.viaproxy.ViaProxy; -import net.raphimc.viaproxy.protocoltranslator.viaproxy.ViaProxyConfig; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; - -import java.io.File; -import java.nio.file.Path; - -@JsonIgnoreProperties(ignoreUnknown = true) -@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final -public class GeyserViaProxyConfiguration extends GeyserJacksonConfiguration { - - private RemoteConfiguration remote = new RemoteConfiguration() { - @Override - public boolean isForwardHost() { - return super.isForwardHost() || !ViaProxy.getConfig().getWildcardDomainHandling().equals(ViaProxyConfig.WildcardDomainHandling.NONE); - } - }; - - @Override - public Path getFloodgateKeyPath() { - return new File(GeyserViaProxyPlugin.ROOT_FOLDER, this.getFloodgateKeyFile()).toPath(); - } - - @Override - public int getPingPassthroughInterval() { - int interval = super.getPingPassthroughInterval(); - if (interval < 15 && ViaProxy.getConfig().getTargetVersion() != null && ViaProxy.getConfig().getTargetVersion().olderThanOrEqualTo(LegacyProtocolVersion.r1_6_4)) { - // <= 1.6.4 servers sometimes block incoming connections from an IP address if too many connections are made - interval = 15; - } - return interval; - } - - @Override - public RemoteConfiguration getRemote() { - return this.remote; - } - -} diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java index 0bfc9d022f2..4478d11ef1c 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java @@ -24,6 +24,7 @@ */ package org.geysermc.geyser.platform.viaproxy; +import com.google.gson.annotations.JsonAdapter; import lombok.Getter; import net.raphimc.viaproxy.ViaProxy; import net.raphimc.viaproxy.plugins.ViaProxyPlugin; @@ -41,7 +42,7 @@ public class GeyserViaProxyDumpInfo extends BootstrapDumpInfo { private final String platformVersion; private final boolean onlineMode; - @AsteriskSerializer.Asterisk(isIp = true) + @JsonAdapter(value = AsteriskSerializer.class) private final String serverIP; private final int serverPort; private final List plugins; diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java index 10f414b51dc..550ee91061d 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java @@ -26,6 +26,7 @@ import net.raphimc.viaproxy.cli.ConsoleFormatter; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.command.GeyserCommandSource; @@ -75,6 +76,13 @@ public void debug(String message) { } } + @Override + public void debug(@Nullable Object object) { + if (this.debug) { + this.logger.debug(ConsoleFormatter.convert(String.valueOf(object))); + } + } + @Override public void setDebug(boolean debug) { this.debug = debug; diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java index b5e614468dd..6030d4f8361 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java @@ -33,6 +33,7 @@ import net.raphimc.viaproxy.plugins.events.ProxyStartEvent; import net.raphimc.viaproxy.plugins.events.ProxyStopEvent; import net.raphimc.viaproxy.plugins.events.ShouldVerifyOnlineModeEvent; +import net.raphimc.viaproxy.protocoltranslator.viaproxy.ViaProxyConfig; import org.apache.logging.log4j.LogManager; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; @@ -43,18 +44,18 @@ import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.ConfigLoader; +import org.geysermc.geyser.configuration.GeyserPluginConfig; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import org.geysermc.geyser.platform.viaproxy.listener.GeyserServerTransferListener; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.LoopbackUtil; +import org.spongepowered.configurate.serialize.SerializationException; import java.io.File; -import java.io.IOException; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; @@ -62,10 +63,10 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootstrap, EventRegistrar { - public static final File ROOT_FOLDER = new File(PluginManager.PLUGINS_DIR, "Geyser"); + private static final File ROOT_FOLDER = new File(PluginManager.PLUGINS_DIR, "Geyser"); private final GeyserViaProxyLogger logger = new GeyserViaProxyLogger(LogManager.getLogger("Geyser")); - private GeyserViaProxyConfiguration config; + private GeyserPluginConfig config; private GeyserImpl geyser; private StandaloneCloudCommandManager cloud; private CommandRegistry commandRegistry; @@ -125,7 +126,7 @@ public void onGeyserInitialize() { return; } - this.geyser = GeyserImpl.load(PlatformType.VIAPROXY, this); + this.geyser = GeyserImpl.load(this); this.geyser.eventBus().register(this, new GeyserServerTransferListener()); LoopbackUtil.checkAndApplyLoopback(this.logger); } @@ -159,7 +160,7 @@ public void onGeyserEnable() { // Only initialize the ping passthrough if the protocol version is above beta 1.7.3, as that's when the status protocol was added this.pingPassthrough = GeyserLegacyPingPassthrough.init(this.geyser); } - if (this.config.getRemote().authType() == AuthType.FLOODGATE) { + if (this.config.java().authType() == AuthType.FLOODGATE) { ViaProxy.getConfig().setPassthroughBungeecordPlayerInfo(true); } } @@ -175,7 +176,12 @@ public void onGeyserShutdown() { } @Override - public GeyserConfiguration getGeyserConfig() { + public PlatformType platformType() { + return PlatformType.VIAPROXY; + } + + @Override + public GeyserPluginConfig config() { return this.config; } @@ -228,18 +234,36 @@ public boolean testFloodgatePluginPresent() { return false; } + @Override + public Path getFloodgateKeyPath() { + return new File(ROOT_FOLDER, config.advanced().floodgateKeyFile()).toPath(); + } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { - try { - final File configFile = FileUtils.fileOrCopiedFromResource(new File(ROOT_FOLDER, "config.yml"), "config.yml", s -> s.replaceAll("generateduuid", UUID.randomUUID().toString()), this); - this.config = FileUtils.loadConfig(configFile, GeyserViaProxyConfiguration.class); - } catch (IOException e) { - this.logger.severe(GeyserLocale.getLocaleStringLog("geyser.config.failed"), e); + this.config = new ConfigLoader(this) + .transformer(node -> { + try { + if (!ViaProxy.getConfig().getWildcardDomainHandling().equals(ViaProxyConfig.WildcardDomainHandling.NONE)) { + node.node("java", "forward-host").set(true); + } + + var pingPassthroughInterval = node.node("ping-passthrough-interval"); + int interval = pingPassthroughInterval.getInt(); + if (interval < 15 && ViaProxy.getConfig().getTargetVersion() != null && ViaProxy.getConfig().getTargetVersion().olderThanOrEqualTo(LegacyProtocolVersion.r1_6_4)) { + // <= 1.6.4 servers sometimes block incoming connections from an IP address if too many connections are made + pingPassthroughInterval.set(15); + } + } catch (SerializationException e) { + throw new RuntimeException(e); + } + }) + .configFile(new File(ROOT_FOLDER, "config.yml")) + .load(GeyserPluginConfig.class); + if (this.config == null) { return false; } - this.config.getRemote().setAuthType(Files.isRegularFile(this.config.getFloodgateKeyPath()) ? AuthType.FLOODGATE : AuthType.OFFLINE); - this.logger.setDebug(this.config.isDebugMode()); - GeyserConfiguration.checkGeyserConfiguration(this.config, this.logger); + this.config.java().authType(Files.isRegularFile(getFloodgateKeyPath()) ? AuthType.FLOODGATE : AuthType.OFFLINE); return true; } diff --git a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts index 093f0a8c098..04803753b88 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -62,6 +62,9 @@ repositories { name = "viaversion" } + // MinecraftAuth + maven("https://maven.lenni0451.net/snapshots") + // Jitpack for e.g. MCPL maven("https://jitpack.io") { content { includeGroupByRegex("com\\.github\\..*") } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index b0ea5fdf6d0..5bb9be1c08d 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -14,10 +14,16 @@ dependencies { api(projects.common) api(projects.api) - // Jackson JSON and YAML serialization - api(libs.bundles.jackson) + api(libs.yaml) // Used for extensions + annotationProcessor(libs.configurate.`interface`.ap) + api(libs.configurate.`interface`) + implementation(libs.configurate.yaml) api(libs.guava) + compileOnly(libs.gson.record.factory) { + isTransitive = false + } + // Fastutil Maps implementation(libs.bundles.fastutil) @@ -26,7 +32,9 @@ dependencies { api(libs.bundles.protocol) - api(libs.minecraftauth) + api(libs.minecraftauth) { + exclude("com.google.code.gson", "gson") + } api(libs.mcprotocollib) { exclude("io.netty", "netty-all") exclude("net.raphimc", "MinecraftAuth") @@ -61,6 +69,7 @@ dependencies { // Test testImplementation(libs.junit) + testImplementation(libs.gson.runtime) // Record support testImplementation(libs.mockito) // Annotation Processors @@ -69,6 +78,8 @@ dependencies { annotationProcessor(projects.ap) api(libs.events) + + api(libs.bstats) } tasks.processResources { diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 7f00075d873..6dcccd738f2 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -45,6 +45,11 @@ public final class Constants { public static final String MINECRAFT_SKIN_SERVER_URL = "https://textures.minecraft.net/texture/"; + public static final int CONFIG_VERSION = 5; + public static final int ADVANCED_CONFIG_VERSION = 1; + + public static final int BSTATS_ID = 5273; + static { URI wsUri = null; try { diff --git a/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java b/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java index 761e67a6725..cde2ff973d8 100644 --- a/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java +++ b/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java @@ -25,14 +25,14 @@ package org.geysermc.geyser; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; +import org.geysermc.geyser.configuration.GeyserConfig; import org.geysermc.geyser.text.GeyserLocale; import java.nio.file.Files; import java.nio.file.Path; public class FloodgateKeyLoader { - public static Path getKeyPath(GeyserJacksonConfiguration config, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) { + public static Path getKeyPath(GeyserConfig config, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) { // Always prioritize Floodgate's key, if it is installed. // This mostly prevents people from trying to copy the key and corrupting it in the process if (floodgateDataFolder != null) { @@ -45,13 +45,7 @@ public static Path getKeyPath(GeyserJacksonConfiguration config, Path floodgateD } } - Path floodgateKey; - if (config.getFloodgateKeyFile().equals("public-key.pem")) { - logger.debug("Floodgate 2.0 doesn't use a public/private key system anymore. We'll search for key.pem instead"); - floodgateKey = geyserDataFolder.resolve("key.pem"); - } else { - floodgateKey = geyserDataFolder.resolve(config.getFloodgateKeyFile()); - } + Path floodgateKey = geyserDataFolder.resolve(config.advanced().floodgateKeyFile()); if (!Files.exists(floodgateKey)) { logger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed")); diff --git a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java index 3063fa4f6e7..ed713cf0c25 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java @@ -27,12 +27,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.CommandRegistry; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.GeyserConfig; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.GeyserWorldManager; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.ping.IGeyserPingPassthrough; +import org.geysermc.geyser.util.metrics.MetricsPlatform; +import org.geysermc.geyser.util.metrics.ProvidedMetricsPlatform; import java.io.InputStream; import java.net.SocketAddress; @@ -68,11 +71,19 @@ public interface GeyserBootstrap { void onGeyserShutdown(); /** - * Returns the current GeyserConfiguration + * Returns the platform type this Geyser instance is running on. * - * @return The current GeyserConfiguration + * @return The current PlatformType */ - GeyserConfiguration getGeyserConfig(); + @NonNull + PlatformType platformType(); + + /** + * Returns the current GeyserConfig + * + * @return The current GeyserConfig + */ + GeyserConfig config(); /** * Returns the current GeyserLogger @@ -189,4 +200,14 @@ default Path getLogsPath() { * Tests if Floodgate is installed, loads the Floodgate key if so, and returns the result of Floodgate installed. */ boolean testFloodgatePluginPresent(); + + /** + * TEMPORARY - will be removed after The Merge:tm:. + */ + Path getFloodgateKeyPath(); + + @Nullable + default MetricsPlatform createMetricsPlatform() { + return new ProvidedMetricsPlatform(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 0a8222f8d72..caaef479d93 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -25,11 +25,8 @@ package org.geysermc.geyser; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import io.netty.channel.epoll.Epoll; import io.netty.util.NettyRuntime; import io.netty.util.concurrent.DefaultThreadFactory; @@ -41,6 +38,11 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; import net.raphimc.minecraftauth.step.msa.StepMsaToken; +import org.bstats.MetricsBase; +import org.bstats.charts.AdvancedPie; +import org.bstats.charts.DrilldownPie; +import org.bstats.charts.SimplePie; +import org.bstats.charts.SingleLineChart; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -70,7 +72,8 @@ import org.geysermc.geyser.api.util.MinecraftVersion; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.CommandRegistry; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.GeyserConfig; +import org.geysermc.geyser.configuration.GeyserPluginConfig; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.erosion.UnixSocketClientListener; import org.geysermc.geyser.event.GeyserEventBus; @@ -95,17 +98,19 @@ import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.AssetUtils; -import org.geysermc.geyser.util.CooldownUtils; -import org.geysermc.geyser.util.Metrics; +import org.geysermc.geyser.util.JsonUtils; import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.geyser.util.NewsHandler; import org.geysermc.geyser.util.VersionCheckUtils; import org.geysermc.geyser.util.WebUtils; +import org.geysermc.geyser.util.metrics.MetricsPlatform; import org.geysermc.mcprotocollib.network.tcp.TcpSession; import java.io.File; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.lang.reflect.Type; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; @@ -130,12 +135,7 @@ @Getter public class GeyserImpl implements GeyserApi, EventRegistrar { - public static final ObjectMapper JSON_MAPPER = new ObjectMapper() - .enable(JsonParser.Feature.IGNORE_UNDEFINED) - .enable(JsonParser.Feature.ALLOW_COMMENTS) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) - .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); + public static final Gson GSON = JsonUtils.createGson(); public static final String NAME = "Geyser"; public static final String GIT_VERSION = BuildData.GIT_VERSION; @@ -168,13 +168,12 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { private ScheduledExecutorService scheduledThread; private GeyserServer geyserServer; - private final PlatformType platformType; private final GeyserBootstrap bootstrap; private final EventBus eventBus; private final GeyserExtensionManager extensionManager; - private Metrics metrics; + private MetricsBase metrics; private PendingMicrosoftAuthentication pendingMicrosoftAuthentication; @Getter(AccessLevel.NONE) @@ -194,12 +193,11 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { @Setter private boolean isEnabled; - private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) { + private GeyserImpl(GeyserBootstrap bootstrap) { instance = this; Geyser.set(this); - this.platformType = platformType; this.bootstrap = bootstrap; /* Initialize event bus */ @@ -269,19 +267,19 @@ public void initialize() { startInstance(); - GeyserConfiguration config = bootstrap.getGeyserConfig(); + GeyserConfig config = bootstrap.config(); double completeTime = (System.currentTimeMillis() - startupTime) / 1000D; String message = GeyserLocale.getLocaleStringLog("geyser.core.finish.done", new DecimalFormat("#.###").format(completeTime)); message += " " + GeyserLocale.getLocaleStringLog("geyser.core.finish.console"); logger.info(message); - if (platformType == PlatformType.STANDALONE) { - if (config.getRemote().authType() != AuthType.FLOODGATE) { + if (platformType() == PlatformType.STANDALONE) { + if (config.java().authType() != AuthType.FLOODGATE) { // If the auth-type is Floodgate, then this Geyser instance is probably owned by the Java server logger.warning(GeyserLocale.getLocaleStringLog("geyser.core.movement_warn")); } - } else if (config.getRemote().authType() == AuthType.FLOODGATE) { + } else if (config.java().authType() == AuthType.FLOODGATE) { VersionCheckUtils.checkForOutdatedFloodgate(logger); } @@ -296,7 +294,7 @@ private void startInstance() { GeyserLocale.finalizeDefaultLocale(this); } GeyserLogger logger = bootstrap.getGeyserLogger(); - GeyserConfiguration config = bootstrap.getGeyserConfig(); + GeyserConfig config = bootstrap.config(); ScoreboardUpdater.init(); @@ -312,33 +310,30 @@ private void startInstance() { boolean portPropertyApplied = false; String pluginUdpAddress = System.getProperty("geyserUdpAddress", System.getProperty("pluginUdpAddress", "")); - if (platformType != PlatformType.STANDALONE) { + if (platformType() != PlatformType.STANDALONE) { int javaPort = bootstrap.getServerPort(); - if (config.getRemote().address().equals("auto")) { - config.setAutoconfiguredRemote(true); - String serverAddress = bootstrap.getServerBindAddress(); - if (!serverAddress.isEmpty() && !"0.0.0.0".equals(serverAddress)) { - config.getRemote().setAddress(serverAddress); - } else { - // Set the remote address to localhost since that is where we are always connecting - try { - config.getRemote().setAddress(InetAddress.getLocalHost().getHostAddress()); - } catch (UnknownHostException ex) { - logger.debug("Unknown host when trying to find localhost."); - if (config.isDebugMode()) { - ex.printStackTrace(); - } - config.getRemote().setAddress(InetAddress.getLoopbackAddress().getHostAddress()); + String serverAddress = bootstrap.getServerBindAddress(); + if (!serverAddress.isEmpty() && !"0.0.0.0".equals(serverAddress)) { + config.java().address(serverAddress); + } else { + // Set the remote address to localhost since that is where we are always connecting + try { + config.java().address(InetAddress.getLocalHost().getHostAddress()); + } catch (UnknownHostException ex) { + logger.debug("Unknown host when trying to find localhost."); + if (config.debugMode()) { + ex.printStackTrace(); } + config.java().address(InetAddress.getLoopbackAddress().getHostAddress()); } - if (javaPort != -1) { - config.getRemote().setPort(javaPort); - } + } + if (javaPort != -1) { + config.java().port(javaPort); } boolean forceMatchServerPort = "server".equals(pluginUdpPort); - if ((config.getBedrock().isCloneRemotePort() || forceMatchServerPort) && javaPort != -1) { - config.getBedrock().setPort(javaPort); + if ((config.bedrock().cloneRemotePort() || forceMatchServerPort) && javaPort != -1) { + config.bedrock().port(javaPort); if (forceMatchServerPort) { if (geyserUdpPort.isEmpty()) { logger.info("Port set from system generic property to match Java server."); @@ -352,15 +347,15 @@ private void startInstance() { if ("server".equals(pluginUdpAddress)) { String address = bootstrap.getServerBindAddress(); if (!address.isEmpty()) { - config.getBedrock().setAddress(address); + config.bedrock().address(address); } } else if (!pluginUdpAddress.isEmpty()) { - config.getBedrock().setAddress(pluginUdpAddress); + config.bedrock().address(pluginUdpAddress); } if (!portPropertyApplied && !pluginUdpPort.isEmpty()) { int port = Integer.parseInt(pluginUdpPort); - config.getBedrock().setPort(port); + config.bedrock().port(port); if (geyserUdpPort.isEmpty()) { logger.info("Port set from generic system property: " + port); } else { @@ -368,16 +363,17 @@ private void startInstance() { } } - if (platformType != PlatformType.VIAPROXY) { + + if (platformType() != PlatformType.VIAPROXY) { boolean floodgatePresent = bootstrap.testFloodgatePluginPresent(); - if (config.getRemote().authType() == AuthType.FLOODGATE && !floodgatePresent) { + if (config.java().authType() == AuthType.FLOODGATE && !floodgatePresent) { logger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); return; - } else if (config.isAutoconfiguredRemote() && floodgatePresent) { + } else if (floodgatePresent) { // Floodgate installed means that the user wants Floodgate authentication logger.debug("Auto-setting to Floodgate authentication."); - config.getRemote().setAuthType(AuthType.FLOODGATE); + config.java().authType(AuthType.FLOODGATE); } } } @@ -390,7 +386,7 @@ private void startInstance() { if (parsedPort < 1 || parsedPort > 65535) { throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!"); } - config.getBedrock().setBroadcastPort(parsedPort); + config.bedrock().broadcastPort(parsedPort); logger.info("Broadcast port set from system property: " + parsedPort); } catch (NumberFormatException e) { logger.error(String.format("Invalid broadcast port from system property: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")")); @@ -398,26 +394,30 @@ private void startInstance() { } // It's set to 0 only if no system property or manual config value was set - if (config.getBedrock().broadcastPort() == 0) { - config.getBedrock().setBroadcastPort(config.getBedrock().port()); + if (config.bedrock().broadcastPort() == 0) { + config.bedrock().broadcastPort(config.bedrock().port()); } - String remoteAddress = config.getRemote().address(); - // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. - if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { - String[] record = WebUtils.findSrvRecord(this, remoteAddress); - if (record != null) { - int remotePort = Integer.parseInt(record[2]); - config.getRemote().setAddress(remoteAddress = record[3]); - config.getRemote().setPort(remotePort); - logger.debug("Found SRV record \"" + remoteAddress + ":" + remotePort + "\""); + if (!(config instanceof GeyserPluginConfig)) { + String remoteAddress = config.java().address(); + // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. + if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { + String[] record = WebUtils.findSrvRecord(this, remoteAddress); + if (record != null) { + int remotePort = Integer.parseInt(record[2]); + config.java().address(remoteAddress = record[3]); + config.java().port(remotePort); + logger.debug("Found SRV record \"" + remoteAddress + ":" + remotePort + "\""); + } } + } else if (!config.advanced().useDirectConnection()) { + logger.warning("The use-direct-connection config option is deprecated. Please reach out to us on Discord if there's a reason it needs to be disabled."); } // Ensure that PacketLib does not create an event loop for handling packets; we'll do that ourselves TcpSession.USE_EVENT_LOOP_FOR_PACKETS = false; - pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.getPendingAuthenticationTimeout()); + pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.pendingAuthenticationTimeout()); this.newsHandler = new NewsHandler(BRANCH, this.buildNumber()); @@ -429,8 +429,7 @@ private void startInstance() { logger.debug("Epoll is not available; Erosion's Unix socket handling will not work."); } - CooldownUtils.setDefaultShowCooldown(config.getShowCooldown()); - BedrockDimension.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether + BedrockDimension.changeBedrockNetherId(config.aboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether Integer bedrockThreadCount = Integer.getInteger("Geyser.BedrockNetworkThreads"); if (bedrockThreadCount == null) { @@ -439,10 +438,10 @@ private void startInstance() { } this.geyserServer = new GeyserServer(this, bedrockThreadCount); - this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port())) + this.geyserServer.bind(new InetSocketAddress(config.bedrock().address(), config.bedrock().port())) .whenComplete((avoid, throwable) -> { - String address = config.getBedrock().address(); - String port = String.valueOf(config.getBedrock().port()); // otherwise we get commas + String address = config.bedrock().address(); + String port = String.valueOf(config.bedrock().port()); // otherwise we get commas if (throwable == null) { if ("0.0.0.0".equals(address)) { @@ -460,9 +459,9 @@ private void startInstance() { } }).join(); - if (config.getRemote().authType() == AuthType.FLOODGATE) { + if (config.java().authType() == AuthType.FLOODGATE) { try { - Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath()); + Key key = new AesKeyProducer().produceFrom(bootstrap.getFloodgateKeyPath()); cipher = new AesCipher(new Base64Topping()); cipher.init(key); logger.debug("Loaded Floodgate key!"); @@ -474,15 +473,42 @@ private void startInstance() { } } - if (config.getMetrics().isEnabled()) { - metrics = new Metrics(this, "GeyserMC", config.getMetrics().getUniqueId(), false, java.util.logging.Logger.getLogger("")); - metrics.addCustomChart(new Metrics.SingleLineChart("players", sessionManager::size)); + MetricsPlatform metricsPlatform = bootstrap.createMetricsPlatform(); + if (metricsPlatform != null && metricsPlatform.enabled()) { + metrics = new MetricsBase( + "server-implementation", + metricsPlatform.serverUuid(), + Constants.BSTATS_ID, + true, // Already checked above. + builder -> { + // OS specific data + String osName = System.getProperty("os.name"); + String osArch = System.getProperty("os.arch"); + String osVersion = System.getProperty("os.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + builder.appendField("osName", osName); + builder.appendField("osArch", osArch); + builder.appendField("osVersion", osVersion); + builder.appendField("coreCount", coreCount); + }, + builder -> {}, + null, + () -> true, + logger::error, + logger::info, + metricsPlatform.logFailedRequests(), + metricsPlatform.logSentData(), + metricsPlatform.logResponseStatusText(), + metricsPlatform.disableRelocateCheck() + ); + metrics.addCustomChart(new SingleLineChart("players", sessionManager::size)); // Prevent unwanted words best we can - metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> config.getRemote().authType().toString().toLowerCase(Locale.ROOT))); - metrics.addCustomChart(new Metrics.SimplePie("platform", platformType::platformName)); - metrics.addCustomChart(new Metrics.SimplePie("defaultLocale", GeyserLocale::getDefaultLocale)); - metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserImpl.VERSION)); - metrics.addCustomChart(new Metrics.AdvancedPie("playerPlatform", () -> { + metrics.addCustomChart(new SimplePie("authMode", () -> config.java().authType().toString().toLowerCase(Locale.ROOT))); + metrics.addCustomChart(new SimplePie("platform", platformType()::platformName)); + metrics.addCustomChart(new SimplePie("defaultLocale", GeyserLocale::getDefaultLocale)); + metrics.addCustomChart(new SimplePie("version", () -> GeyserImpl.VERSION)); + metrics.addCustomChart(new AdvancedPie("playerPlatform", () -> { Map valueMap = new HashMap<>(); for (GeyserSession session : sessionManager.getAllSessions()) { if (session == null) continue; @@ -496,7 +522,7 @@ private void startInstance() { } return valueMap; })); - metrics.addCustomChart(new Metrics.AdvancedPie("playerVersion", () -> { + metrics.addCustomChart(new AdvancedPie("playerVersion", () -> { Map valueMap = new HashMap<>(); for (GeyserSession session : sessionManager.getAllSessions()) { if (session == null) continue; @@ -515,10 +541,10 @@ private void startInstance() { if (minecraftVersion != null) { Map> versionMap = new HashMap<>(); Map platformMap = new HashMap<>(); - platformMap.put(platformType.platformName(), 1); + platformMap.put(platformType().platformName(), 1); versionMap.put(minecraftVersion, platformMap); - metrics.addCustomChart(new Metrics.DrilldownPie("minecraftServerVersion", () -> { + metrics.addCustomChart(new DrilldownPie("minecraftServerVersion", () -> { // By the end, we should return, for example: // 1.16.5 => (Spigot, 1) return versionMap; @@ -527,7 +553,7 @@ private void startInstance() { // The following code can be attributed to the PaperMC project // https://github.com/PaperMC/Paper/blob/master/Spigot-Server-Patches/0005-Paper-Metrics.patch#L614 - metrics.addCustomChart(new Metrics.DrilldownPie("javaVersion", () -> { + metrics.addCustomChart(new DrilldownPie("javaVersion", () -> { Map> map = new HashMap<>(); String javaVersion = System.getProperty("java.version"); Map entry = new HashMap<>(); @@ -562,25 +588,25 @@ private void startInstance() { metrics = null; } - if (config.getRemote().authType() == AuthType.ONLINE) { + if (config.java().authType() == AuthType.ONLINE) { // May be written/read to on multiple threads from each GeyserSession as well as writing the config savedAuthChains = new ConcurrentHashMap<>(); + Type type = new TypeToken>() { }.getType(); // TODO Remove after a while - just a migration help //noinspection deprecation File refreshTokensFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); if (refreshTokensFile.exists()) { logger.info("Migrating refresh tokens to auth chains..."); - TypeReference> type = new TypeReference<>() { }; Map refreshTokens = null; - try { - refreshTokens = JSON_MAPPER.readValue(refreshTokensFile, type); + try (FileReader reader = new FileReader(refreshTokensFile)) { + refreshTokens = GSON.fromJson(reader, type); } catch (IOException e) { // ignored - we'll just delete this file :)) } if (refreshTokens != null) { - List validUsers = config.getSavedUserLogins(); + List validUsers = config.savedUserLogins(); final Gson gson = new Gson(); for (Map.Entry entry : refreshTokens.entrySet()) { String user = entry.getKey(); @@ -615,16 +641,14 @@ private void startInstance() { File authChainsFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); if (authChainsFile.exists()) { - TypeReference> type = new TypeReference<>() { }; - Map authChainFile = null; - try { - authChainFile = JSON_MAPPER.readValue(authChainsFile, type); + try (FileReader reader = new FileReader(authChainsFile)) { + authChainFile = GSON.fromJson(reader, type); } catch (IOException e) { logger.error("Cannot load saved user tokens!", e); } if (authChainFile != null) { - List validUsers = config.getSavedUserLogins(); + List validUsers = config.savedUserLogins(); boolean doWrite = false; for (Map.Entry entry : authChainFile.entrySet()) { String user = entry.getKey(); @@ -652,7 +676,7 @@ private void startInstance() { this.eventBus.fire(new GeyserPostInitializeEvent(this.extensionManager, this.eventBus)); } - if (config.isNotifyOnNewBedrockUpdate()) { + if (config.notifyOnNewBedrockUpdate()) { VersionCheckUtils.checkForGeyserUpdate(this::getLogger); } } @@ -803,13 +827,13 @@ public EventBus eventBus() { @NonNull public RemoteServer defaultRemoteServer() { - return getConfig().getRemote(); + return config().java(); } @Override @NonNull public BedrockListener bedrockListener() { - return getConfig().getBedrock(); + return config().bedrock(); } @Override @@ -827,7 +851,7 @@ public Path packDirectory() { @Override @NonNull public PlatformType platformType() { - return platformType; + return bootstrap.platformType(); } @Override @@ -858,9 +882,9 @@ public int buildNumber() { return Integer.parseInt(BUILD_NUMBER); } - public static GeyserImpl load(PlatformType platformType, GeyserBootstrap bootstrap) { + public static GeyserImpl load(GeyserBootstrap bootstrap) { if (instance == null) { - return new GeyserImpl(platformType, bootstrap); + return new GeyserImpl(bootstrap); } return instance; @@ -883,8 +907,8 @@ public GeyserLogger getLogger() { return bootstrap.getGeyserLogger(); } - public GeyserConfiguration getConfig() { - return bootstrap.getGeyserConfig(); + public GeyserConfig config() { + return bootstrap.config(); } public WorldManager getWorldManager() { @@ -897,7 +921,7 @@ public String authChainFor(@NonNull String bedrockName) { } public void saveAuthChain(@NonNull String bedrockName, @NonNull String authChain) { - if (!getConfig().getSavedUserLogins().contains(bedrockName)) { + if (!config().savedUserLogins().contains(bedrockName)) { // Do not save this login return; } @@ -919,11 +943,9 @@ private void scheduleAuthChainsWrite() { scheduledThread.execute(() -> { // Ensure all writes are handled on the same thread File savedAuthChains = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); - TypeReference> type = new TypeReference<>() { }; + Type type = new TypeToken>() { }.getType(); try (FileWriter writer = new FileWriter(savedAuthChains)) { - JSON_MAPPER.writerFor(type) - .withDefaultPrettyPrinter() - .writeValue(writer, this.savedAuthChains); + GSON.toJson(this.savedAuthChains, type, writer); } catch (IOException e) { getLogger().error("Unable to write saved refresh tokens!", e); } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index f408de29c02..a49391babe7 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -100,7 +100,10 @@ default void info(Component message) { * @param object the object to log */ default void debug(@Nullable Object object) { - debug(String.valueOf(object)); + if (isDebug()) { + // Don't create String object by default + info(String.valueOf(object)); + } } /** diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java index 838ab71b1f5..18a42216581 100644 --- a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java @@ -163,7 +163,7 @@ public CommandRegistry(GeyserImpl geyser, CommandManager cl registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); registerBuiltInCommand(new PingCommand("ping", "geyser.commands.ping.desc", "geyser.command.ping")); - if (this.geyser.getPlatformType() == PlatformType.STANDALONE) { + if (this.geyser.platformType() == PlatformType.STANDALONE) { registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java index d2066dba179..b416b04e689 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java @@ -25,12 +25,13 @@ package org.geysermc.geyser.command.defaults; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.GeyserConfig; import org.geysermc.geyser.util.LoopbackUtil; import org.geysermc.geyser.util.WebUtils; import org.incendo.cloud.CommandManager; @@ -79,7 +80,7 @@ public void execute(CommandContext context) { // Replace "<" and ">" symbols if they are present to avoid the common issue of people including them final String ip = ipArgument.replace("<", "").replace(">", ""); - final int port = portArgument != null ? portArgument : geyser.getConfig().getBedrock().broadcastPort(); // default bedrock port + final int port = portArgument != null ? portArgument : geyser.config().bedrock().broadcastPort(); // default bedrock port // Issue: people commonly checking placeholders if (ip.equals("ip")) { @@ -105,41 +106,41 @@ public void execute(CommandContext context) { return; } - GeyserConfiguration config = geyser.getConfig(); + GeyserConfig config = geyser.config(); // Issue: do the ports not line up? We only check this if players don't override the broadcast port - if they do, they (hopefully) know what they're doing - if (config.getBedrock().broadcastPort() == config.getBedrock().port()) { - if (port != config.getBedrock().port()) { + if (config.bedrock().broadcastPort() == config.bedrock().port()) { + if (port != config.bedrock().port()) { if (portArgument != null) { source.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration (" - + config.getBedrock().port() + ")"); + + config.bedrock().port() + ")"); source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config."); - if (config.getBedrock().isCloneRemotePort()) { + if (config.bedrock().cloneRemotePort()) { source.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead."); } } else { source.sendMessage("You did not specify the port to check (add it with \":\"), " + "and the default port 19132 does not match the port in your Geyser configuration (" - + config.getBedrock().port() + ")!"); + + config.bedrock().port() + ")!"); source.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`."); } } } else { - if (config.getBedrock().broadcastPort() != port) { + if (config.bedrock().broadcastPort() != port) { source.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration (" - + config.getBedrock().broadcastPort() + "). "); + + config.bedrock().broadcastPort() + "). "); source.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on."); source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config."); } } // Issue: is the `bedrock` `address` in the config different? - if (!config.getBedrock().address().equals("0.0.0.0")) { + if (!config.bedrock().address().equals("0.0.0.0")) { source.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional."); } // Issue: did someone turn on enable-proxy-protocol, and they didn't mean it? - if (config.getBedrock().isEnableProxyProtocol()) { + if (config.bedrock().enableProxyProtocol()) { source.sendMessage("You have the `enable-proxy-protocol` setting enabled. " + "Unless you're deliberately using additional software that REQUIRES this setting, you may not need it enabled."); } @@ -171,7 +172,7 @@ public void execute(CommandContext context) { CONNECTION_TEST_MOTD = connectionTestMotd; source.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait..."); - JsonNode output; + JsonObject output; try { String hostname = URLEncoder.encode(ip, StandardCharsets.UTF_8); output = WebUtils.getJson("https://checker.geysermc.org/ping?hostname=" + hostname + "&port=" + port); @@ -179,18 +180,18 @@ public void execute(CommandContext context) { CONNECTION_TEST_MOTD = null; } - if (output.get("success").asBoolean()) { - JsonNode cache = output.get("cache"); + if (output.get("success").getAsBoolean()) { + JsonObject cache = output.getAsJsonObject("cache"); String when; - if (cache.get("fromCache").asBoolean()) { - when = cache.get("secondsSince").asInt() + " seconds ago"; + if (cache.get("fromCache").isJsonPrimitive()) { + when = cache.get("secondsSince").getAsBoolean() + " seconds ago"; } else { when = "now"; } - JsonNode ping = output.get("ping"); - JsonNode pong = ping.get("pong"); - String remoteMotd = pong.get("motd").asText(); + JsonObject ping = output.getAsJsonObject("ping"); + JsonObject pong = ping.getAsJsonObject("pong"); + String remoteMotd = pong.get("motd").getAsString(); if (!connectionTestMotd.equals(remoteMotd)) { source.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " + "Did you supply the correct IP and port of your server?"); @@ -198,7 +199,7 @@ public void execute(CommandContext context) { return; } - if (ping.get("tcpFirst").asBoolean()) { + if (ping.get("tcpFirst").getAsBoolean()) { source.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information."); sendLinks(source); return; @@ -210,9 +211,9 @@ public void execute(CommandContext context) { } source.sendMessage("Your server is likely unreachable from outside the network!"); - JsonNode message = output.get("message"); - if (message != null && !message.asText().isEmpty()) { - source.sendMessage("Got the error message: " + message.asText()); + JsonElement message = output.get("message"); + if (message != null && !message.getAsString().isEmpty()) { + source.sendMessage("Got the error message: " + message.getAsString()); } sendLinks(source); } catch (Exception e) { diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java index fc46f01088d..6a71ca6955d 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java @@ -25,10 +25,9 @@ package org.geysermc.geyser.command.defaults; -import com.fasterxml.jackson.core.util.DefaultIndenter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; @@ -37,6 +36,7 @@ import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.JsonUtils; import org.geysermc.geyser.util.WebUtils; import org.incendo.cloud.CommandManager; import org.incendo.cloud.context.CommandContext; @@ -55,7 +55,6 @@ public class DumpCommand extends GeyserCommand { private static final Iterable SUGGESTIONS = List.of("full", "offline", "logs"); private final GeyserImpl geyser; - private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String DUMP_URL = "https://dump.geysermc.org/"; public DumpCommand(GeyserImpl geyser, String name, String description, String permission) { @@ -110,20 +109,14 @@ public void execute(CommandContext context) { AsteriskSerializer.showSensitive = showSensitive; + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", source.locale())); String dumpData; try { DumpInfo dump = new DumpInfo(geyser, addLog); - - if (offlineDump) { - DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); - // Make arrays easier to read - prettyPrinter.indentArraysWith(new DefaultIndenter(" ", "\n")); - dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(dump); - } else { - dumpData = MAPPER.writeValueAsString(dump); - } - } catch (IOException e) { + dumpData = gson.toJson(dump); + } catch (Exception e) { source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.collect_error_short"), e); return; @@ -149,10 +142,10 @@ public void execute(CommandContext context) { source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", source.locale())); String response; - JsonNode responseNode; + JsonObject responseNode; try { response = WebUtils.post(DUMP_URL + "documents", dumpData); - responseNode = MAPPER.readTree(response); + responseNode = JsonUtils.parseJson(response); } catch (IOException e) { source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.upload_error_short"), e); @@ -160,11 +153,11 @@ public void execute(CommandContext context) { } if (!responseNode.has("key")) { - source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", source.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response)); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", source.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").getAsString() : response)); return; } - uploadedDumpUrl = DUMP_URL + responseNode.get("key").asText(); + uploadedDumpUrl = DUMP_URL + responseNode.get("key").getAsString(); } source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", source.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl); diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java index 8d34c1bf047..0f25377600c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.command.defaults; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; @@ -73,7 +73,7 @@ public void execute(CommandContext context) { GeyserImpl.NAME, GeyserImpl.VERSION, javaVersions, bedrockVersions)); // Disable update checking in dev mode and for players in Geyser Standalone - if (!GeyserImpl.getInstance().isProductionEnvironment() || (!source.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) { + if (!GeyserImpl.getInstance().isProductionEnvironment() || (!source.isConsole() && geyser.platformType() == PlatformType.STANDALONE)) { return; } @@ -85,8 +85,8 @@ public void execute(CommandContext context) { source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", source.locale())); try { int buildNumber = this.geyser.buildNumber(); - JsonNode response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest"); - int latestBuildNumber = response.get("build").asInt(); + JsonObject response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest"); + int latestBuildNumber = response.get("build").getAsInt(); if (latestBuildNumber == buildNumber) { source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", source.locale())); diff --git a/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java index edacd49fff9..a8e1ee9dd78 100644 --- a/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java @@ -25,18 +25,16 @@ package org.geysermc.geyser.command.standalone; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; import java.util.Collections; import java.util.Set; @Getter -@JsonIgnoreProperties(ignoreUnknown = true) -@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final +@ConfigSerializable +@SuppressWarnings("FieldMayBeFinal") public class PermissionConfiguration { - @JsonProperty("default-permissions") private Set defaultPermissions = Collections.emptySet(); } diff --git a/core/src/main/java/org/geysermc/geyser/configuration/AdvancedConfig.java b/core/src/main/java/org/geysermc/geyser/configuration/AdvancedConfig.java new file mode 100644 index 00000000000..de3db005f96 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/AdvancedConfig.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import org.geysermc.geyser.Constants; +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 java.util.UUID; + +@ConfigSerializable +public interface AdvancedConfig { + // Cannot be type File yet because we may want to hide it in plugin instances. + @Comment(""" + Floodgate uses encryption to ensure use from authorized sources. + This should point to the public key generated by Floodgate (BungeeCord, Spigot or Velocity) + You can ignore this when not using Floodgate. + If you're using a plugin version of Floodgate on the same server, the key will automatically be picked up from Floodgate.""") + @DefaultString("key.pem") + String floodgateKeyFile(); + + @Comment(""" + The maximum number of custom skulls to be displayed per player. Increasing this may decrease performance on weaker devices. + Setting this to -1 will cause all custom skulls to be displayed regardless of distance or number.""") + @DefaultNumeric(128) + int maxVisibleCustomSkulls(); + + @Comment("The radius in blocks around the player in which custom skulls are displayed.") + @DefaultNumeric(32) + int customSkullRenderDistance(); + + @Comment(""" + Specify how many days player skin images will be cached to disk to save downloading them from the internet. + A value of 0 is disabled. (Default: 0)""") + int cacheImages(); + + @Comment(""" + Which item to use to mark unavailable slots in a Bedrock player inventory. Examples of this are the 2x2 crafting grid while in creative, + or custom inventory menus with sizes different from the usual 3x9. A barrier block is the default item. + This config option can be set to any Bedrock item identifier. If you want to set this to a custom item, make sure that you specify the item in the following format: "geyser_custom:" + """) + @DefaultString("minecraft:barrier") + String unusableSpaceBlock(); + + @Comment(""" + Geyser updates the Scoreboard after every Scoreboard packet, but when Geyser tries to handle + a lot of scoreboard packets per second, this can cause serious lag. + This option allows you to specify after how many Scoreboard packets per seconds + the Scoreboard updates will be limited to four updates per second.""") + @DefaultNumeric(20) + int scoreboardPacketThreshold(); + + @Comment(""" + The internet supports a maximum MTU of 1492 but could cause issues with packet fragmentation. + 1400 is the default.""") + @DefaultNumeric(1400) + int mtu(); + + @Comment(""" + Whether to connect directly into the Java server without creating a TCP connection. + This should only be disabled if a plugin that interfaces with packets or the network does not work correctly with Geyser. + If enabled, the remote address and port sections are ignored + If disabled, expect performance decrease and latency increase + """) + @DefaultBoolean(true) + @PluginSpecific + boolean useDirectConnection(); + + @Comment(""" + Whether Geyser should attempt to disable compression for Bedrock players. This should be a benefit as there is no need to compress data + when Java packets aren't being handled over the network. + This requires use-direct-connection to be true. + """) + @DefaultBoolean(true) + @PluginSpecific + boolean disableCompression(); + + @Comment("Do not touch!") + @ExcludePlatform(platforms = {"BungeeCord", "Spigot", "Velocity"}) // bStats platform versions used + default UUID metricsUuid() { + return UUID.randomUUID(); + } + + @Comment("Do not touch!") + default int version() { + return Constants.ADVANCED_CONFIG_VERSION; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/ConfigLoader.java b/core/src/main/java/org/geysermc/geyser/configuration/ConfigLoader.java new file mode 100644 index 00000000000..68c4c2dd8c3 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/ConfigLoader.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import com.google.common.annotations.VisibleForTesting; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.GeyserBootstrap; +import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.text.GeyserLocale; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.NodePath; +import org.spongepowered.configurate.interfaces.InterfaceDefaultOptions; +import org.spongepowered.configurate.objectmapping.meta.Processor; +import org.spongepowered.configurate.transformation.ConfigurationTransformation; +import org.spongepowered.configurate.yaml.NodeStyle; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.spongepowered.configurate.NodePath.path; +import static org.spongepowered.configurate.transformation.TransformAction.remove; +import static org.spongepowered.configurate.transformation.TransformAction.rename; + +public final class ConfigLoader { + private static final String HEADER = """ + -------------------------------- + Geyser Configuration File + + A bridge between Minecraft: Bedrock Edition and Minecraft: Java Edition. + + GitHub: https://github.com/GeyserMC/Geyser + Discord: https://discord.gg/geysermc + Wiki: https://wiki.geysermc.org/ + + NOTICE: See https://wiki.geysermc.org/geyser/setup/ for the setup guide. Many video tutorials are outdated. + In most cases, especially with server hosting providers, further hosting-specific configuration is required. + --------------------------------"""; + + private static final String ADVANCED_HEADER = """ + -------------------------------- + Geyser ADVANCED Configuration File + + In most cases, you do *not* need to mess with this file to get Geyser running. + Tread with caution. + -------------------------------- + """; + + /** + * Only nullable for testing. + */ + private final @Nullable GeyserBootstrap bootstrap; + private @Nullable Consumer transformer; + private File configFile; + + public ConfigLoader(GeyserBootstrap bootstrap) { + this.bootstrap = bootstrap; + configFile = new File(bootstrap.getConfigFolder().toFile(), "config.yml"); + } + + @VisibleForTesting + ConfigLoader(File file) { + this.bootstrap = null; + configFile = file; + } + + /** + * Creates the directory as indicated by {@link GeyserBootstrap#getConfigFolder()} + */ + @This + public ConfigLoader createFolder() { + Path dataFolder = this.bootstrap.getConfigFolder(); + if (!dataFolder.toFile().exists()) { + //noinspection ResultOfMethodCallIgnored + dataFolder.toFile().mkdir(); + } + return this; + } + + @This + public ConfigLoader transformer(Consumer transformer) { + this.transformer = transformer; + return this; + } + + @This + public ConfigLoader configFile(File configFile) { + this.configFile = configFile; + return this; + } + + /** + * @return null if the config failed to load. + */ + @Nullable + public T load(Class configClass) { + try { + return load0(configClass); + } catch (IOException ex) { + bootstrap.getGeyserLogger().error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); + return null; + } + } + + private T load0(Class configClass) throws IOException { + var loader = createLoader(configFile, HEADER); + + CommentedConfigurationNode node = loader.load(); + boolean originallyEmpty = !configFile.exists() || node.isNull(); + + var migrations = ConfigurationTransformation.versionedBuilder() + .versionKey("config-version") + // Pre-Configurate + .addVersion(5, ConfigurationTransformation.builder() + .addAction(path("legacy-ping-passthrough"), configClass == GeyserRemoteConfig.class ? remove() : (path, value) -> { + // Invert value + value.set(!value.getBoolean()); + return new Object[]{"integrated-ping-passthrough"}; + }) + .addAction(path("remote"), rename("java")) + .addAction(path("floodgate-key-file"), (path, value) -> { + // Elimate any legacy config values + if ("public-key.pem".equals(value.getString())) { + value.set("key.pem"); + } + return null; + }) + .addAction(path("default-locale"), (path, value) -> { + if (value.getString() == null) { + value.set("system"); + } + return null; + }) + .addAction(path("show-cooldown"), (path, value) -> { + String s = value.getString(); + if (s != null) { + switch (s) { + case "true" -> value.set("title"); + case "false" -> value.set("disabled"); + } + } + return null; + }) + .addAction(path("metrics", "uuid"), (path, value) -> { + if ("generateduuid".equals(value.getString())) { + // Manually copied config without Metrics UUID creation? + value.set(UUID.randomUUID()); + } + return null; + }) + .addAction(path("remote", "address"), (path, value) -> { + if ("auto".equals(value.getString())) { + // Auto-convert back to localhost + value.set("127.0.0.1"); + } + return null; + }) + .addAction(path("metrics", "enabled"), (path, value) -> { + // Move to the root, not in the Metrics class. + return new Object[]{"enable-metrics"}; + }) + .addAction(path("bedrock", "motd1"), rename("primary-motd")) + .addAction(path("bedrock", "motd2"), rename("secondary-motd")) + .build()) + .build(); + + int currentVersion = migrations.version(node); + migrations.apply(node); + int newVersion = migrations.version(node); + + T config = node.get(configClass); + + // Serialize the instance to ensure strict field ordering. Additionally, if we serialized back + // to the old node, existing nodes would only have their value changed, keeping their position + // at the top of the ordered map, forcing all new nodes to the bottom (regardless of field order). + // For that reason, we must also create a new node. + CommentedConfigurationNode newRoot = CommentedConfigurationNode.root(loader.defaultOptions()); + newRoot.set(config); + + // Create the path in a way that Standalone changing the config name will be fine. + int extensionIndex = configFile.getName().lastIndexOf("."); + File advancedConfigPath = new File(configFile.getParent(), configFile.getName().substring(0, extensionIndex) + "-advanced" + configFile.getName().substring(extensionIndex)); + AdvancedConfig advancedConfig = null; + + if (originallyEmpty || currentVersion != newVersion) { + + if (!originallyEmpty && currentVersion > 4) { + // Only copy comments over if the file already existed, and we are going to replace it + + // Second case: Version 4 is pre-configurate where there were commented out nodes. + // These get treated as comments on lower nodes, which produces very undesirable results. + + ConfigurationCommentMover.moveComments(node, newRoot); + } else if (currentVersion <= 4) { + advancedConfig = migrateToAdvancedConfig(advancedConfigPath, node); + } + + loader.save(newRoot); + } + if (advancedConfig == null) { + advancedConfig = loadAdvancedConfig(advancedConfigPath); + } + + if (transformer != null) { + // We transform AFTER saving so that these specific transformations aren't applied to file. + transformer.accept(newRoot); + config = newRoot.get(configClass); + } + + config.advanced(advancedConfig); + + if (this.bootstrap != null) { // Null for testing only. + this.bootstrap.getGeyserLogger().setDebug(config.debugMode()); + } + + return config; + } + + private AdvancedConfig migrateToAdvancedConfig(File file, ConfigurationNode configRoot) throws IOException { + Stream copyFromOldConfig = Stream.of("max-visible-custom-skulls", "custom-skull-render-distance", "scoreboard-packet-threshold", "mtu", + "floodgate-key-file", "use-direct-connection", "disable-compression") + .map(NodePath::path); + + var loader = createLoader(file, ADVANCED_HEADER); + + CommentedConfigurationNode advancedNode = CommentedConfigurationNode.root(loader.defaultOptions()); + copyFromOldConfig.forEach(path -> { + ConfigurationNode node = configRoot.node(path); + if (!node.virtual()) { + advancedNode.node(path).mergeFrom(node); + configRoot.removeChild(path); + } + }); + + ConfigurationNode metricsUuid = configRoot.node("metrics", "uuid"); + if (!metricsUuid.virtual()) { + advancedNode.node("metrics-uuid").set(metricsUuid.get(UUID.class)); + } + + advancedNode.node("version").set(Constants.ADVANCED_CONFIG_VERSION); + + AdvancedConfig advancedConfig = advancedNode.get(AdvancedConfig.class); + // Ensure all fields get populated + CommentedConfigurationNode newNode = CommentedConfigurationNode.root(loader.defaultOptions()); + newNode.set(advancedConfig); + loader.save(newNode); + return advancedConfig; + } + + private AdvancedConfig loadAdvancedConfig(File file) throws IOException { + var loader = createLoader(file, ADVANCED_HEADER); + if (file.exists()) { + ConfigurationNode node = loader.load(); + return node.get(AdvancedConfig.class); + } else { + ConfigurationNode node = CommentedConfigurationNode.root(loader.defaultOptions()); + node.node("version").set(Constants.ADVANCED_CONFIG_VERSION); + AdvancedConfig advancedConfig = node.get(AdvancedConfig.class); + node.set(advancedConfig); + loader.save(node); + return advancedConfig; + } + } + + private YamlConfigurationLoader createLoader(File file, String header) { + return YamlConfigurationLoader.builder() + .file(file) + .indent(2) + .nodeStyle(NodeStyle.BLOCK) + .defaultOptions(options -> InterfaceDefaultOptions.addTo(options, builder -> { + if (this.bootstrap != null) { // Testing only. + builder.addProcessor(ExcludePlatform.class, excludePlatform(bootstrap.platformType().platformName())) + .addProcessor(PluginSpecific.class, integrationSpecific(bootstrap.platformType() != PlatformType.STANDALONE)); + } + }) + .shouldCopyDefaults(false) // If we use ConfigurationNode#get(type, default), do not write the default back to the node. + .header(header) + .serializers(builder -> builder.register(new LowercaseEnumSerializer()))) + .build(); + } + + private static Processor.Factory excludePlatform(String thisPlatform) { + return (data, fieldType) -> (value, destination) -> { + for (String platform : data.platforms()) { + if (thisPlatform.equals(platform)) { + //noinspection DataFlowIssue + destination.parent().removeChild(destination.key()); + break; + } + } + }; + } + + private static Processor.Factory integrationSpecific(boolean thisConfigPlugin) { + return (data, fieldType) -> (value, destination) -> { + if (data.forPlugin() != thisConfigPlugin) { + //noinspection DataFlowIssue + destination.parent().removeChild(destination.key()); + } + }; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/ConfigurationCommentMover.java b/core/src/main/java/org/geysermc/geyser/configuration/ConfigurationCommentMover.java new file mode 100644 index 00000000000..4aed64da738 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/ConfigurationCommentMover.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationVisitor; + +/** + * Moves comments from a different node and puts them on this node + */ +public final class ConfigurationCommentMover implements ConfigurationVisitor.Stateless { + + private final CommentedConfigurationNode otherRoot; + + private ConfigurationCommentMover(@NonNull CommentedConfigurationNode otherNode) { + this.otherRoot = otherNode; + } + + @Override + public void enterNode(final ConfigurationNode node) { + if (!(node instanceof CommentedConfigurationNode destination)) { + // Should not occur because all nodes in a tree are the same type, + // and our static method below ensures this visitor is only used on CommentedConfigurationNodes + throw new IllegalStateException(node.path() + " is not a CommentedConfigurationNode"); + } + // Node with the same path + CommentedConfigurationNode source = otherRoot.node(node.path()); + + moveSingle(source, destination); + } + + private static void moveSingle(@NonNull CommentedConfigurationNode source, @NonNull CommentedConfigurationNode destination) { + // Only transfer the comment, overriding if necessary + String comment = source.comment(); + if (comment != null) { + destination.comment(comment); + } + } + + /** + * Moves comments from a source node and its children to a destination node and its children (of a different tree), overriding if necessary. + * Comments are only moved to the destination node and its children which exist. + * Comments are only moved to and from nodes with the exact same path. + * + * @param source the source of the comments, which must be the topmost parent of a tree + * @param destination the destination of the comments, any node in a different tree + */ + public static void moveComments(@NonNull CommentedConfigurationNode source, @NonNull CommentedConfigurationNode destination) { + if (source.parent() != null) { + throw new IllegalArgumentException("source is not the base of the tree it is within: " + source.path()); + } + + if (source.isNull()) { + // It has no value(s), but may still have a comment on it. Don't both traversing the whole destination tree. + moveSingle(source, destination); + } else { + destination.visit(new ConfigurationCommentMover(source)); + } + } +} diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/ExcludePlatform.java similarity index 67% rename from bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneConfiguration.java rename to core/src/main/java/org/geysermc/geyser/configuration/ExcludePlatform.java index 1102ed0a998..1308872c318 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/ExcludePlatform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,20 +23,15 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.platform.standalone; +package org.geysermc.geyser.configuration; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -import java.nio.file.Path; -import java.nio.file.Paths; - -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public final class GeyserStandaloneConfiguration extends GeyserJacksonConfiguration { - @Override - public Path getFloodgateKeyPath() { - return Paths.get(getFloodgateKeyFile()); - } +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExcludePlatform { + String[] platforms(); } diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfig.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfig.java new file mode 100644 index 00000000000..050a511f667 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfig.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.api.network.AuthType; +import org.geysermc.geyser.api.network.BedrockListener; +import org.geysermc.geyser.api.network.RemoteServer; +import org.geysermc.geyser.network.GameProtocol; +import org.geysermc.geyser.text.AsteriskSerializer; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.CooldownUtils; +import org.spongepowered.configurate.interfaces.meta.Exclude; +import org.spongepowered.configurate.interfaces.meta.Field; +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.interfaces.meta.range.NumericRange; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.Collections; +import java.util.List; + +@ConfigSerializable +public interface GeyserConfig { + BedrockConfig bedrock(); + + JavaConfig java(); + + @Comment(""" + For online mode authentication type only. + Stores a list of Bedrock player usernames that should have their Java Edition account saved after login. + This saves a token that can be reused to authenticate the player later. This does not save emails or passwords, + but you should still be cautious when adding to this list and giving others access to this Geyser instance's files. + Removing a name from this list will delete its cached login information on the next Geyser startup. + The file that tokens will be saved in is in the same folder as this config, named "saved-refresh-tokens.json".""") + default List savedUserLogins() { + return List.of("ThisExampleUsernameShouldBeLongEnoughToNeverBeAnXboxUsername", + "ThisOtherExampleUsernameShouldAlsoBeLongEnough"); + } + + @Comment(""" + Specify how many seconds to wait while user authorizes Geyser to access their Microsoft account. + User is allowed to disconnect from the server during this period.""") + @DefaultNumeric(128) + int pendingAuthenticationTimeout(); + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + @Comment(""" + Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands. + Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients.""") + @DefaultBoolean(true) + boolean commandSuggestions(); + + @Comment("Relay the MOTD from the Java server to Bedrock players.") + @DefaultBoolean(true) + boolean passthroughMotd(); + + @Comment("Relay the player count and max players from the Java server to Bedrock players.") + @DefaultBoolean(true) + boolean passthroughPlayerCounts(); + + @Comment(""" + Use server API methods to determine the Java server's MOTD and ping passthrough. + There is no need to disable this unless your MOTD or player count does not appear properly.""") + @DefaultBoolean(true) + @PluginSpecific + boolean integratedPingPassthrough(); + + @Comment("How often to ping the Java server to refresh MOTD and player count, in seconds.") + @DefaultNumeric(3) + int pingPassthroughInterval(); + + @Comment(""" + Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate + ping, it may also cause players to time out more easily.""") + boolean forwardPlayerPing(); + + @Comment(""" + Maximum amount of players that can connect. + This is only visual, and is only applied if passthrough-motd is disabled.""") + @DefaultNumeric(100) + int maxPlayers(); + + @Comment("If debug messages should be sent through console") + boolean debugMode(); + + @Comment(""" + Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat. + Please note: if the cooldown is enabled, some users may see a black box during the cooldown sequence, like below: + https://geysermc.org/img/external/cooldown_indicator.png + This can be disabled by going into Bedrock settings under the accessibility tab and setting "Text Background Opacity" to 0 + This setting can be set to "title", "actionbar" or "false\"""") + default CooldownUtils.CooldownType showCooldown() { + return CooldownUtils.CooldownType.TITLE; + } + + @Comment("Controls if coordinates are shown to players.") + @DefaultBoolean(true) + boolean showCoordinates(); + + @Comment("Whether Bedrock players are blocked from performing their scaffolding-style bridging.") + boolean disableBedrockScaffolding(); + + @Comment("The default locale if we don't have the one the client requested. If set to \"system\", the system's language will be used.") + @NonNull + @DefaultString(GeyserLocale.SYSTEM_LOCALE) + String defaultLocale(); + + @Comment("Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices.") + @DefaultBoolean(true) + boolean allowCustomSkulls(); + + @Comment(""" + Whether to add any items and blocks which normally does not exist in Bedrock Edition. + This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching. + If this is disabled, furnace minecart items will be mapped to hopper minecart items. + Geyser's block, item, and skull mappings systems will also be disabled. + This option requires a restart of Geyser in order to change its setting.""") + @DefaultBoolean(true) + boolean addNonBedrockItems(); + + @Comment(""" + Bedrock prevents building and displaying blocks above Y127 in the Nether. + This config option works around that by changing the Nether dimension ID to the End ID. + The main downside to this is that the entire Nether will have the same red fog rather than having different fog for each biome.""") + boolean aboveBedrockNetherBuilding(); + + @Comment(""" + Force clients to load all resource packs if there are any. + If set to false, it allows the user to connect to the server even if they don't + want to download the resource packs.""") + @DefaultBoolean(true) + boolean forceResourcePacks(); + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + @Comment(""" + Allows Xbox achievements to be unlocked. + If a player types in an unknown command, they will receive a message that states cheats are disabled. + Otherwise, commands work as expected.""") + boolean xboxAchievementsEnabled(); + + @Comment("Whether player IP addresses will be logged by the server.") + @DefaultBoolean(true) + boolean logPlayerIpAddresses(); + + @Comment(""" + Whether to alert the console and operators that a new Geyser version is available that supports a Bedrock version + that this Geyser version does not support. It's recommended to keep this option enabled, as many Bedrock platforms + auto-update.""") + @DefaultBoolean(true) + boolean notifyOnNewBedrockUpdate(); + + @Comment(""" + bStats is a stat tracker that is entirely anonymous and tracks only basic information + about Geyser, such as how many people are online, how many servers are using Geyser, + what OS is being used, etc. You can learn more about bStats here: https://bstats.org/. + https://bstats.org/plugin/server-implementation/GeyserMC""") + @DefaultBoolean(true) + @ExcludePlatform(platforms = {"BungeeCord", "Spigot", "Velocity"}) // bStats platform versions used + boolean enableMetrics(); + + /** + * A separate config file added to this class manually. + */ + @Field + @NonNull + AdvancedConfig advanced(); + + @Field + void advanced(AdvancedConfig config); + + @ConfigSerializable + interface BedrockConfig extends BedrockListener { + @Override + @Comment(""" + The IP address that will listen for connections. + Generally, you should only uncomment and change this if you want to limit what IPs can connect to your server.""") + @NonNull + @DefaultString("0.0.0.0") + @AsteriskSerializer.Asterisk + String address(); + + @Override + @Comment("The port that will listen for connections") + @DefaultNumeric(19132) + @NumericRange(from = 0, to = 65535) + int port(); + + @Override + @Comment(""" + The port to broadcast to Bedrock clients with the MOTD that they should use to connect to the server. + DO NOT change this unless Geyser runs on a different internal port than the one that is used to connect.""") + @DefaultNumeric(0) + @NumericRange(from = 0, to = 65535) + int broadcastPort(); + + @Comment(""" + Some hosting services change your Java port everytime you start the server and require the same port to be used for Bedrock. + This option makes the Bedrock port the same as the Java port every time you start the server.""") + @DefaultBoolean + @PluginSpecific + boolean cloneRemotePort(); + + void address(String address); + + void port(int port); + + void broadcastPort(int broadcastPort); + + @Override + @Comment(""" + The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true. + If either of these are empty, the respective string will default to "Geyser\"""") + @DefaultString("Geyser") + String primaryMotd(); + + @Override + @DefaultString("Another Geyser server.") + String secondaryMotd(); + + @Override + @Comment("The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu.") + @DefaultString("Geyser") + String serverName(); + + @Comment(""" + How much to compress network traffic to the Bedrock client. The higher the number, the more CPU usage used, but + the smaller the bandwidth used. Does not have any effect below -1 or above 9. Set to -1 to disable.""") + @DefaultNumeric(6) + @NumericRange(from = -1, to = 9) + int compressionLevel(); + + @Comment(""" + Whether to enable PROXY protocol or not for clients. You DO NOT WANT this feature unless you run UDP reverse proxy + in front of your Geyser instance.""") + @DefaultBoolean + boolean enableProxyProtocol(); + + @Comment(""" + A list of allowed PROXY protocol speaking proxy IP addresses/subnets. Only effective when "enable-proxy-protocol" is enabled, and + should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.). + Keeping this list empty means there is no IP address whitelist. + IP addresses, subnets, and links to plain text files are supported.""") + default List proxyProtocolWhitelistedIps() { + return Collections.emptyList(); + } + } + + @ConfigSerializable + interface JavaConfig extends RemoteServer { + + void address(String address); + + void port(int port); + + @Override + @Comment(""" + What type of authentication Bedrock players will be checked against when logging into the Java server. + Can be "floodgate" (see https://wiki.geysermc.org/floodgate/), "online", or "offline".""") + @NonNull + default AuthType authType() { + return AuthType.ONLINE; + } + + void authType(AuthType authType); + + @Comment(""" + Whether to enable PROXY protocol or not while connecting to the server. + This is useful only when: + 1) Your server supports PROXY protocol (it probably doesn't) + 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config. + IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT!""") + boolean useProxyProtocol(); + + boolean forwardHostname(); + + @Override + @Exclude + default String minecraftVersion() { + return GameProtocol.getJavaMinecraftVersion(); + } + + @Override + @Exclude + default int protocolVersion() { + return GameProtocol.getJavaProtocolVersion(); + } + + @Override + @Exclude + default boolean resolveSrv() { + return false; + } + } + + @Comment(""" + Allow connections from ProxyPass and Waterdog. + See https://www.spigotmc.org/wiki/firewall-guide/ for assistance - use UDP instead of TCP.""") + // if u have offline mode enabled pls be safe + boolean enableProxyConnections(); + + @Comment("Do not change!") + @SuppressWarnings("unused") + default int configVersion() { + return Constants.CONFIG_VERSION; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java deleted file mode 100644 index 88bb98171e6..00000000000 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.configuration; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.geysermc.geyser.GeyserLogger; -import org.geysermc.geyser.api.network.AuthType; -import org.geysermc.geyser.api.network.BedrockListener; -import org.geysermc.geyser.api.network.RemoteServer; -import org.geysermc.geyser.network.CIDRMatcher; -import org.geysermc.geyser.network.GameProtocol; -import org.geysermc.geyser.text.GeyserLocale; - -import java.nio.file.Path; -import java.util.List; - -public interface GeyserConfiguration { - /** - * If the config was originally 'auto' before the values changed - */ - void setAutoconfiguredRemote(boolean autoconfiguredRemote); - - // Modify this when you introduce breaking changes into the config - int CURRENT_CONFIG_VERSION = 4; - - IBedrockConfiguration getBedrock(); - - IRemoteConfiguration getRemote(); - - List getSavedUserLogins(); - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - boolean isCommandSuggestions(); - - @JsonIgnore - boolean isPassthroughMotd(); - - @JsonIgnore - boolean isPassthroughPlayerCounts(); - - @JsonIgnore - boolean isLegacyPingPassthrough(); - - int getPingPassthroughInterval(); - - boolean isForwardPlayerPing(); - - int getMaxPlayers(); - - boolean isDebugMode(); - - @Deprecated - boolean isAllowThirdPartyCapes(); - - @Deprecated - boolean isAllowThirdPartyEars(); - - String getShowCooldown(); - - boolean isShowCoordinates(); - - boolean isDisableBedrockScaffolding(); - - EmoteOffhandWorkaroundOption getEmoteOffhandWorkaround(); - - String getDefaultLocale(); - - Path getFloodgateKeyPath(); - - boolean isAddNonBedrockItems(); - - boolean isAboveBedrockNetherBuilding(); - - boolean isForceResourcePacks(); - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - boolean isXboxAchievementsEnabled(); - - int getCacheImages(); - - boolean isAllowCustomSkulls(); - - int getMaxVisibleCustomSkulls(); - - int getCustomSkullRenderDistance(); - - boolean isLogPlayerIpAddresses(); - - boolean isNotifyOnNewBedrockUpdate(); - - String getUnusableSpaceBlock(); - - IMetricsInfo getMetrics(); - - int getPendingAuthenticationTimeout(); - - boolean isAutoconfiguredRemote(); - - interface IBedrockConfiguration extends BedrockListener { - void setAddress(String address); - - void setPort(int port); - - void setBroadcastPort(int broadcastPort); - - boolean isCloneRemotePort(); - - int getCompressionLevel(); - - boolean isEnableProxyProtocol(); - - List getProxyProtocolWhitelistedIPs(); - - /** - * @return Unmodifiable list of {@link CIDRMatcher}s from {@link #getProxyProtocolWhitelistedIPs()} - */ - List getWhitelistedIPsMatchers(); - } - - interface IRemoteConfiguration extends RemoteServer { - - void setAddress(String address); - - void setPort(int port); - - boolean isUseProxyProtocol(); - - boolean isForwardHost(); - - default String minecraftVersion() { - return GameProtocol.getJavaMinecraftVersion(); - } - - default int protocolVersion() { - return GameProtocol.getJavaProtocolVersion(); - } - - void setAuthType(AuthType authType); - } - - interface IMetricsInfo { - - boolean isEnabled(); - - String getUniqueId(); - } - - int getScoreboardPacketThreshold(); - - // if u have offline mode enabled pls be safe - boolean isEnableProxyConnections(); - - int getMtu(); - - boolean isUseDirectConnection(); - - boolean isDisableCompression(); - - int getConfigVersion(); - - static void checkGeyserConfiguration(GeyserConfiguration geyserConfig, GeyserLogger geyserLogger) { - if (geyserConfig.getConfigVersion() < CURRENT_CONFIG_VERSION) { - geyserLogger.warning(GeyserLocale.getLocaleStringLog("geyser.bootstrap.config.outdated")); - } else if (geyserConfig.getConfigVersion() > CURRENT_CONFIG_VERSION) { - geyserLogger.warning(GeyserLocale.getLocaleStringLog("geyser.bootstrap.config.too_new")); - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java index 1af3578a348..48490fad07d 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserCustomSkullConfiguration.java @@ -25,26 +25,21 @@ package org.geysermc.geyser.configuration; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; import java.util.Collections; import java.util.List; import java.util.Objects; -@JsonIgnoreProperties(ignoreUnknown = true) +@ConfigSerializable @SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final public class GeyserCustomSkullConfiguration { - @JsonProperty("player-usernames") private List playerUsernames; - @JsonProperty("player-uuids") private List playerUUIDs; - @JsonProperty("player-profiles") private List playerProfiles; - @JsonProperty("skin-hashes") private List skinHashes; public List getPlayerUsernames() { @@ -62,4 +57,14 @@ public List getPlayerProfiles() { public List getPlayerSkinHashes() { return Objects.requireNonNullElse(skinHashes, Collections.emptyList()); } + + @Override + public String toString() { + return "GeyserCustomSkullConfiguration{" + + "playerUsernames=" + playerUsernames + + ", playerUUIDs=" + playerUUIDs + + ", playerProfiles=" + playerProfiles + + ", skinHashes=" + skinHashes + + '}'; + } } diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java deleted file mode 100644 index 81ac824e432..00000000000 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.configuration; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.Getter; -import lombok.Setter; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.network.AuthType; -import org.geysermc.geyser.network.CIDRMatcher; -import org.geysermc.geyser.text.AsteriskSerializer; -import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.util.WebUtils; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final -public abstract class GeyserJacksonConfiguration implements GeyserConfiguration { - - @Setter - private boolean autoconfiguredRemote = false; - - private BedrockConfiguration bedrock = new BedrockConfiguration(); - private RemoteConfiguration remote = new RemoteConfiguration(); - - @JsonProperty("saved-user-logins") - private List savedUserLogins = Collections.emptyList(); - - @JsonProperty("floodgate-key-file") - private String floodgateKeyFile = "key.pem"; - - public abstract Path getFloodgateKeyPath(); - - @JsonProperty("command-suggestions") - private boolean commandSuggestions = true; - - @JsonProperty("passthrough-motd") - private boolean isPassthroughMotd = false; - - @JsonProperty("passthrough-player-counts") - private boolean isPassthroughPlayerCounts = false; - - @JsonProperty("legacy-ping-passthrough") - private boolean isLegacyPingPassthrough = false; - - @JsonProperty("ping-passthrough-interval") - private int pingPassthroughInterval = 3; - - @JsonProperty("forward-player-ping") - private boolean forwardPlayerPing = false; - - @JsonProperty("max-players") - private int maxPlayers = 100; - - @JsonProperty("debug-mode") - private boolean debugMode = false; - - @JsonProperty("allow-third-party-capes") - private boolean allowThirdPartyCapes = false; - - @JsonProperty("show-cooldown") - private String showCooldown = "title"; - - @JsonProperty("show-coordinates") - private boolean showCoordinates = true; - - @JsonProperty("disable-bedrock-scaffolding") - private boolean disableBedrockScaffolding = false; - - @JsonDeserialize(using = EmoteOffhandWorkaroundOption.Deserializer.class) - @JsonProperty("emote-offhand-workaround") - private EmoteOffhandWorkaroundOption emoteOffhandWorkaround = EmoteOffhandWorkaroundOption.DISABLED; - - @JsonProperty("allow-third-party-ears") - private boolean allowThirdPartyEars = false; - - @JsonProperty("default-locale") - private String defaultLocale = null; // is null by default so system language takes priority - - @JsonProperty("cache-images") - private int cacheImages = 0; - - @JsonProperty("allow-custom-skulls") - private boolean allowCustomSkulls = true; - - @JsonProperty("max-visible-custom-skulls") - private int maxVisibleCustomSkulls = 128; - - @JsonProperty("custom-skull-render-distance") - private int customSkullRenderDistance = 32; - - @JsonProperty("add-non-bedrock-items") - private boolean addNonBedrockItems = true; - - @JsonProperty("above-bedrock-nether-building") - private boolean aboveBedrockNetherBuilding = false; - - @JsonProperty("force-resource-packs") - private boolean forceResourcePacks = true; - - @JsonProperty("xbox-achievements-enabled") - private boolean xboxAchievementsEnabled = false; - - @JsonProperty("log-player-ip-addresses") - private boolean logPlayerIpAddresses = true; - - @JsonProperty("notify-on-new-bedrock-update") - private boolean notifyOnNewBedrockUpdate = true; - - @JsonProperty("unusable-space-block") - private String unusableSpaceBlock = "minecraft:barrier"; - - private MetricsInfo metrics = new MetricsInfo(); - - @JsonProperty("pending-authentication-timeout") - private int pendingAuthenticationTimeout = 120; - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class BedrockConfiguration implements IBedrockConfiguration { - @AsteriskSerializer.Asterisk(isIp = true) - @JsonProperty("address") - @Setter - private String address = "0.0.0.0"; - - @Override - public @NonNull String address() { - return address; - } - - @Setter - @JsonProperty("port") - private int port = 19132; - - @Override - public int port() { - return port; - } - - @Setter - @JsonProperty("broadcast-port") - private int broadcastPort = 0; - - @Override - public int broadcastPort() { - return broadcastPort; - } - - @Getter - @JsonProperty("clone-remote-port") - private boolean cloneRemotePort = false; - - @JsonProperty("motd1") - private String motd1 = "GeyserMC"; - - @Override - public String primaryMotd() { - return motd1; - } - - @JsonProperty("motd2") - private String motd2 = "Geyser"; - - @Override - public String secondaryMotd() { - return motd2; - } - - @JsonProperty("server-name") - private String serverName = GeyserImpl.NAME; - - @Override - public String serverName() { - return serverName; - } - - @JsonProperty("compression-level") - private int compressionLevel = 6; - - public int getCompressionLevel() { - return Math.max(-1, Math.min(compressionLevel, 9)); - } - - @Getter - @JsonProperty("enable-proxy-protocol") - private boolean enableProxyProtocol = false; - - @Getter - @JsonProperty("proxy-protocol-whitelisted-ips") - private List proxyProtocolWhitelistedIPs = Collections.emptyList(); - - @JsonIgnore - private List whitelistedIPsMatchers = null; - - @Override - public List getWhitelistedIPsMatchers() { - // Effective Java, Third Edition; Item 83: Use lazy initialization judiciously - List matchers = this.whitelistedIPsMatchers; - if (matchers == null) { - synchronized (this) { - // Check if proxyProtocolWhitelistedIPs contains URLs we need to fetch and parse by line - List whitelistedCIDRs = new ArrayList<>(); - for (String ip: proxyProtocolWhitelistedIPs) { - if (!ip.startsWith("http")) { - whitelistedCIDRs.add(ip); - continue; - } - - WebUtils.getLineStream(ip).forEach(whitelistedCIDRs::add); - } - - this.whitelistedIPsMatchers = matchers = whitelistedCIDRs.stream() - .map(CIDRMatcher::new) - .collect(Collectors.toList()); - } - } - return Collections.unmodifiableList(matchers); - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class RemoteConfiguration implements IRemoteConfiguration { - @Setter - @AsteriskSerializer.Asterisk(isIp = true) - @JsonProperty("address") - private String address = "auto"; - - @Override - public String address() { - return address; - } - - @JsonDeserialize(using = PortDeserializer.class) - @Setter - @JsonProperty("port") - private int port = 25565; - - @Override - public int port() { - return port; - } - - @Setter - @JsonDeserialize(using = AuthTypeDeserializer.class) - @JsonProperty("auth-type") - private AuthType authType = AuthType.ONLINE; - - @Override - public @NonNull AuthType authType() { - return authType; - } - - @Override - public boolean resolveSrv() { - return false; - } - - @Getter - @JsonProperty("use-proxy-protocol") - private boolean useProxyProtocol = false; - - @Getter - @JsonProperty("forward-hostname") - private boolean forwardHost = false; - } - - @Getter - @JsonIgnoreProperties(ignoreUnknown = true) - public static class MetricsInfo implements IMetricsInfo { - private boolean enabled = true; - - @JsonDeserialize(using = MetricsIdDeserializer.class) - @JsonProperty("uuid") - private String uniqueId = UUID.randomUUID().toString(); - - private static class MetricsIdDeserializer extends JsonDeserializer { - @Override - public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - String uuid = p.getValueAsString(); - if ("generateduuid".equals(uuid)) { - // Compensate for configs not copied from the jar - return UUID.randomUUID().toString(); - } - return uuid; - } - } - } - - @JsonProperty("scoreboard-packet-threshold") - private int scoreboardPacketThreshold = 10; - - @JsonProperty("enable-proxy-connections") - private boolean enableProxyConnections = false; - - @JsonProperty("mtu") - private int mtu = 1400; - - @JsonProperty("use-direct-connection") - private boolean useDirectConnection = true; - - @JsonProperty("disable-compression") - private boolean isDisableCompression = true; - - @JsonProperty("config-version") - private int configVersion = 0; - - /** - * Ensure that the port deserializes in the config as a number no matter what. - */ - protected static class PortDeserializer extends JsonDeserializer { - @Override - public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - String value = p.getValueAsString(); - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - System.err.println(GeyserLocale.getLocaleStringLog("geyser.bootstrap.config.invalid_port")); - return 25565; - } - } - } - - public static class AuthTypeDeserializer extends JsonDeserializer { - @Override - public AuthType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return AuthType.getByName(p.getValueAsString()); - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserPluginConfig.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserPluginConfig.java new file mode 100644 index 00000000000..c08875baa4e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserPluginConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import org.spongepowered.configurate.interfaces.meta.Exclude; +import org.spongepowered.configurate.interfaces.meta.Field; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public interface GeyserPluginConfig extends GeyserConfig { + @Override + IntegratedJavaConfig java(); + + @ConfigSerializable + interface IntegratedJavaConfig extends JavaConfig { + @Override + @Field + String address(); + + @Override + void address(String address); + + @Override + @Field + int port(); + + @Override + void port(int port); + + @Override + @Exclude + default boolean forwardHostname() { + return true; // No need to worry about suspicious behavior flagging the server. + } + } + + @Comment(""" + How often to ping the Java server to refresh MOTD and player count, in seconds. + Only relevant if integrated-ping-passthrough is disabled.""") + @Override + int pingPassthroughInterval(); +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserRemoteConfig.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserRemoteConfig.java new file mode 100644 index 00000000000..67496e5a17f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserRemoteConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import org.geysermc.geyser.text.AsteriskSerializer; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; +import org.spongepowered.configurate.interfaces.meta.range.NumericRange; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +/** + * Used for any instance where the Java server is detached from Geyser. + */ +@ConfigSerializable +public interface GeyserRemoteConfig extends GeyserConfig { + @Override + RemoteConfig java(); + + @ConfigSerializable + interface RemoteConfig extends JavaConfig { + @Override + @Comment("The IP address of the Java Edition server.") + @DefaultString("127.0.0.1") + @AsteriskSerializer.Asterisk + String address(); + + @Override + @Comment("The port of the Java Edition server.") + @DefaultNumeric(25565) + @NumericRange(from = 0, to = 65535) + int port(); + + @Override + @Comment(""" + Forward the hostname that the Bedrock client used to connect over to the Java server + This is designed to be used for forced hosts on proxies""") + boolean forwardHostname(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/LowercaseEnumSerializer.java b/core/src/main/java/org/geysermc/geyser/configuration/LowercaseEnumSerializer.java new file mode 100644 index 00000000000..5414ab2e20d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/configuration/LowercaseEnumSerializer.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import io.leangen.geantyref.TypeToken; +import org.spongepowered.configurate.serialize.ScalarSerializer; +import org.spongepowered.configurate.serialize.Scalars; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.lang.reflect.Type; +import java.util.Locale; +import java.util.function.Predicate; + +/** + * Ensures enum values are written to lowercase. {@link Scalars#ENUM} will read enum values + * in any case. + */ +final class LowercaseEnumSerializer extends ScalarSerializer> { + LowercaseEnumSerializer() { + super(new TypeToken>() {}); + } + + @Override + public Enum deserialize(Type type, Object obj) throws SerializationException { + return Scalars.ENUM.deserialize(type, obj); + } + + @Override + protected Object serialize(Enum item, Predicate> typeSupported) { + return item.name().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/configuration/EmoteOffhandWorkaroundOption.java b/core/src/main/java/org/geysermc/geyser/configuration/PluginSpecific.java similarity index 58% rename from core/src/main/java/org/geysermc/geyser/configuration/EmoteOffhandWorkaroundOption.java rename to core/src/main/java/org/geysermc/geyser/configuration/PluginSpecific.java index fd44d390389..91871687acd 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/EmoteOffhandWorkaroundOption.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/PluginSpecific.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,26 +25,17 @@ package org.geysermc.geyser.configuration; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -import java.io.IOException; - -public enum EmoteOffhandWorkaroundOption { - NO_EMOTES, - EMOTES_AND_OFFHAND, - DISABLED; - - public static class Deserializer extends JsonDeserializer { - @Override - public EmoteOffhandWorkaroundOption deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - String value = p.getValueAsString(); - return switch (value) { - case "no-emotes" -> NO_EMOTES; - case "emotes-and-offhand" -> EMOTES_AND_OFFHAND; - default -> DISABLED; - }; - } - } +/** + * Add to a config value to indicate this field is only for plugin versions of Geyser, + * or vice-versa. + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PluginSpecific { + boolean forPlugin() default true; } diff --git a/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java index 7851fadfd3c..e373309a4ff 100644 --- a/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java +++ b/core/src/main/java/org/geysermc/geyser/dump/BootstrapDumpInfo.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.dump; +import com.google.gson.annotations.JsonAdapter; import lombok.AllArgsConstructor; import lombok.Getter; import org.geysermc.geyser.GeyserImpl; @@ -38,7 +39,7 @@ public class BootstrapDumpInfo { private final PlatformType platform; public BootstrapDumpInfo() { - this.platform = GeyserImpl.getInstance().getPlatformType(); + this.platform = GeyserImpl.getInstance().platformType(); } @Getter @@ -55,7 +56,7 @@ public static class PluginInfo { @AllArgsConstructor public static class ListenerInfo { - @AsteriskSerializer.Asterisk(isIp = true) + @JsonAdapter(value = AsteriskSerializer.class) public String ip; public int port; } diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java index 515e1a6296a..e4d71850f0d 100644 --- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java +++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java @@ -25,28 +25,36 @@ package org.geysermc.geyser.dump; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.hash.Hashing; import com.google.common.io.ByteSource; import com.google.common.io.Files; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.google.gson.annotations.SerializedName; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.geysermc.floodgate.util.DeviceOs; -import org.geysermc.floodgate.util.FloodgateInfoHolder; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.extension.Extension; -import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.util.CpuUtils; import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.WebUtils; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; +import org.spongepowered.configurate.interfaces.InterfaceDefaultOptions; +import org.spongepowered.configurate.serialize.SerializationException; import java.io.File; import java.io.IOException; @@ -56,12 +64,15 @@ import java.net.Socket; import java.net.UnknownHostException; import java.nio.file.Paths; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.stream.Collectors; @Getter public class DumpInfo { - @JsonIgnore private static final long MEGABYTE = 1024L * 1024L; private final DumpInfo.VersionInfo versionInfo; @@ -70,8 +81,8 @@ public class DumpInfo { private final Locale systemLocale; private final String systemEncoding; private final GitInfo gitInfo; - private final GeyserConfiguration config; - private final Floodgate floodgate; + private Object config; + private Object advancedConfig; private final Object2IntMap userPlatforms; private final int connectionAttempts; private final HashInfo hashInfo; @@ -91,8 +102,25 @@ public DumpInfo(GeyserImpl geyser, boolean addLog) { this.gitInfo = new GitInfo(GeyserImpl.BUILD_NUMBER, GeyserImpl.COMMIT.substring(0, 7), GeyserImpl.COMMIT, GeyserImpl.BRANCH, GeyserImpl.REPOSITORY); - this.config = geyser.getConfig(); - this.floodgate = new Floodgate(); + try { + // Workaround for JsonAdapter not being allowed on methods + ConfigurationOptions options = InterfaceDefaultOptions.addTo(ConfigurationOptions.defaults(), builder -> + builder.addProcessor(AsteriskSerializer.Asterisk.class, String.class, AsteriskSerializer.CONFIGURATE_SERIALIZER)) + .shouldCopyDefaults(false); + + ConfigurationNode configNode = CommentedConfigurationNode.root(options); + configNode.set(geyser.config()); + this.config = toGson(configNode); + + ConfigurationNode advancedConfigNode = CommentedConfigurationNode.root(options); + advancedConfigNode.set(geyser.config().advanced()); + this.advancedConfig = toGson(advancedConfigNode); + } catch (SerializationException e) { + e.printStackTrace(); + if (geyser.config().debugMode()) { + e.printStackTrace(); + } + } String md5Hash = "unknown"; String sha256Hash = "unknown"; @@ -107,7 +135,7 @@ public DumpInfo(GeyserImpl geyser, boolean addLog) { //noinspection UnstableApiUsage sha256Hash = byteSource.hash(Hashing.sha256()).toString(); } catch (Exception e) { - if (this.config.isDebugMode()) { + if (geyser.config().debugMode()) { e.printStackTrace(); } } @@ -141,6 +169,36 @@ public DumpInfo(GeyserImpl geyser, boolean addLog) { } } + private JsonElement toGson(ConfigurationNode node) { + if (node.isMap()) { + JsonObject object = new JsonObject(); + node.childrenMap().forEach((key, value) -> { + JsonElement json = toGson(value); + object.add(key.toString(), json); + }); + return object; + } else if (node.isList()) { + JsonArray array = new JsonArray(); + node.childrenList().forEach(childNode -> array.add(toGson(childNode))); + return array; + } else { + return convertRawScalar(node); + } + } + + private JsonElement convertRawScalar(ConfigurationNode node) { + final @Nullable Object value = node.rawScalar(); + if (value == null) { + return JsonNull.INSTANCE; + } else if (value instanceof Number n) { + return new JsonPrimitive(n); + } else if (value instanceof Boolean b) { + return new JsonPrimitive(b); + } else { + return new JsonPrimitive(value.toString()); + } + } + @Getter public static class VersionInfo { private final String name; @@ -233,17 +291,6 @@ public static class MCInfo { } } - @Getter - public static class Floodgate { - private final Properties gitInfo; - private final Object config; - - Floodgate() { - this.gitInfo = FloodgateInfoHolder.getGitProperties(); - this.config = FloodgateInfoHolder.getConfig(); - } - } - @Getter public static class LogsInfo { private String link; @@ -253,9 +300,9 @@ public LogsInfo(GeyserImpl geyser) { Map fields = new HashMap<>(); fields.put("content", FileUtils.readAllLines(geyser.getBootstrap().getLogsPath()).collect(Collectors.joining("\n"))); - JsonNode logData = GeyserImpl.JSON_MAPPER.readTree(WebUtils.postForm("https://api.mclo.gs/1/log", fields)); + JsonObject logData = new JsonParser().parse(WebUtils.postForm("https://api.mclo.gs/1/log", fields)).getAsJsonObject(); - this.link = logData.get("url").textValue(); + this.link = logData.get("url").getAsString(); } catch (IOException ignored) { } } } @@ -283,7 +330,7 @@ public FlagsInfo() { public record ExtensionInfo(boolean enabled, String name, String version, String apiVersion, String main, List authors) { } - public record GitInfo(String buildNumber, @JsonProperty("git.commit.id.abbrev") String commitHashAbbrev, @JsonProperty("git.commit.id") String commitHash, - @JsonProperty("git.branch") String branchName, @JsonProperty("git.remote.origin.url") String originUrl) { + public record GitInfo(String buildNumber, @SerializedName("git.commit.id.abbrev") String commitHashAbbrev, @SerializedName("git.commit.id") String commitHash, + @SerializedName("git.branch") String branchName, @SerializedName("git.remote.origin.url") String originUrl) { } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java index 5b4dc096903..c4dae54110c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java @@ -70,7 +70,7 @@ public void translateMetadata(T entity, EntityMetadata bind(InetSocketAddress address) { @@ -166,12 +170,12 @@ private void modifyHandlers(ChannelFuture future) { .addAfter(RakServerOfflineHandler.NAME, RakPingHandler.NAME, new RakPingHandler(this)); // Add proxy handler - boolean isProxyProtocol = this.geyser.getConfig().getBedrock().isEnableProxyProtocol(); + boolean isProxyProtocol = this.geyser.config().bedrock().enableProxyProtocol(); if (isProxyProtocol) { channel.pipeline().addFirst("proxy-protocol-decoder", new ProxyServerHandler()); } - boolean isWhitelistedProxyProtocol = isProxyProtocol && !this.geyser.getConfig().getBedrock().getProxyProtocolWhitelistedIPs().isEmpty(); + boolean isWhitelistedProxyProtocol = isProxyProtocol && !this.geyser.config().bedrock().proxyProtocolWhitelistedIps().isEmpty(); if (Boolean.parseBoolean(System.getProperty("Geyser.RakRateLimitingDisabled", "false")) || isWhitelistedProxyProtocol) { // We would already block any non-whitelisted IP addresses in onConnectionRequest so we can remove the rate limiter channel.pipeline().remove(RakServerRateLimiter.NAME); @@ -201,7 +205,7 @@ public void shutdown() { } private ServerBootstrap createBootstrap() { - if (this.geyser.getConfig().isDebugMode()) { + if (this.geyser.config().debugMode()) { this.geyser.getLogger().debug("EventLoop type: " + TRANSPORT.datagramChannel()); if (TRANSPORT.datagramChannel() == NioDatagramChannel.class) { if (System.getProperties().contains("disableNativeEventLoop")) { @@ -216,7 +220,7 @@ private ServerBootstrap createBootstrap() { GeyserServerInitializer serverInitializer = new GeyserServerInitializer(this.geyser); playerGroup = serverInitializer.getEventLoopGroup(); - this.geyser.getLogger().debug("Setting MTU to " + this.geyser.getConfig().getMtu()); + this.geyser.getLogger().debug("Setting MTU to " + this.geyser.config().advanced().mtu()); int rakPacketLimit = positivePropOrDefault("Geyser.RakPacketLimit", DEFAULT_PACKET_LIMIT); this.geyser.getLogger().debug("Setting RakNet packet limit to " + rakPacketLimit); @@ -231,7 +235,7 @@ private ServerBootstrap createBootstrap() { .channelFactory(RakChannelFactory.server(TRANSPORT.datagramChannel())) .group(group, childGroup) .option(RakChannelOption.RAK_HANDLE_PING, true) - .option(RakChannelOption.RAK_MAX_MTU, this.geyser.getConfig().getMtu()) + .option(RakChannelOption.RAK_MAX_MTU, this.geyser.config().advanced().mtu()) .option(RakChannelOption.RAK_PACKET_LIMIT, rakPacketLimit) .option(RakChannelOption.RAK_GLOBAL_PACKET_LIMIT, rakGlobalPacketLimit) .option(RakChannelOption.RAK_SEND_COOKIE, rakSendCookie) @@ -239,10 +243,10 @@ private ServerBootstrap createBootstrap() { } public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) { - List allowedProxyIPs = geyser.getConfig().getBedrock().getProxyProtocolWhitelistedIPs(); - if (geyser.getConfig().getBedrock().isEnableProxyProtocol() && !allowedProxyIPs.isEmpty()) { + List allowedProxyIPs = geyser.config().bedrock().proxyProtocolWhitelistedIps(); + if (geyser.config().bedrock().enableProxyProtocol() && !allowedProxyIPs.isEmpty()) { boolean isWhitelistedIP = false; - for (CIDRMatcher matcher : geyser.getConfig().getBedrock().getWhitelistedIPsMatchers()) { + for (CIDRMatcher matcher : getWhitelistedIPsMatchers()) { if (matcher.matches(inetSocketAddress.getAddress())) { isWhitelistedIP = true; break; @@ -256,8 +260,8 @@ public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) { } String ip; - if (geyser.getConfig().isLogPlayerIpAddresses()) { - if (geyser.getConfig().getBedrock().isEnableProxyProtocol()) { + if (geyser.config().logPlayerIpAddresses()) { + if (geyser.config().bedrock().enableProxyProtocol()) { ip = this.proxiedAddresses.getOrDefault(inetSocketAddress, inetSocketAddress).toString(); } else { ip = inetSocketAddress.toString(); @@ -283,10 +287,10 @@ public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) { } public BedrockPong onQuery(Channel channel, InetSocketAddress inetSocketAddress) { - if (geyser.getConfig().isDebugMode() && PRINT_DEBUG_PINGS) { + if (geyser.config().debugMode() && PRINT_DEBUG_PINGS) { String ip; - if (geyser.getConfig().isLogPlayerIpAddresses()) { - if (geyser.getConfig().getBedrock().isEnableProxyProtocol()) { + if (geyser.config().logPlayerIpAddresses()) { + if (geyser.config().bedrock().enableProxyProtocol()) { ip = this.proxiedAddresses.getOrDefault(inetSocketAddress, inetSocketAddress).toString(); } else { ip = inetSocketAddress.toString(); @@ -297,10 +301,10 @@ public BedrockPong onQuery(Channel channel, InetSocketAddress inetSocketAddress) geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.network.pinged", ip)); } - GeyserConfiguration config = geyser.getConfig(); + GeyserConfig config = geyser.config(); GeyserPingInfo pingInfo = null; - if (config.isPassthroughMotd() || config.isPassthroughPlayerCounts()) { + if (config.passthroughMotd() || config.passthroughPlayerCounts()) { IGeyserPingPassthrough pingPassthrough = geyser.getBootstrap().getGeyserPingPassthrough(); if (pingPassthrough != null) { pingInfo = pingPassthrough.getPingInformation(inetSocketAddress); @@ -317,25 +321,25 @@ public BedrockPong onQuery(Channel channel, InetSocketAddress inetSocketAddress) .ipv6Port(this.broadcastPort) .serverId(channel.config().getOption(RakChannelOption.RAK_GUID)); - if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { + if (config.passthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); - String mainMotd = (motd.length > 0) ? motd[0] : config.getBedrock().primaryMotd(); // First line of the motd. - String subMotd = (motd.length > 1) ? motd[1] : config.getBedrock().secondaryMotd(); // Second line of the motd if present, otherwise default. + String mainMotd = (motd.length > 0) ? motd[0] : config.bedrock().primaryMotd(); // First line of the motd. + String subMotd = (motd.length > 1) ? motd[1] : config.bedrock().secondaryMotd(); // Second line of the motd if present, otherwise default. pong.motd(mainMotd.trim()); pong.subMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit. } else { - pong.motd(config.getBedrock().primaryMotd()); - pong.subMotd(config.getBedrock().secondaryMotd()); + pong.motd(config.bedrock().primaryMotd()); + pong.subMotd(config.bedrock().secondaryMotd()); } // Placed here to prevent overriding values set in the ping event. - if (config.isPassthroughPlayerCounts() && pingInfo != null) { + if (config.passthroughPlayerCounts() && pingInfo != null) { pong.playerCount(pingInfo.getPlayers().getOnline()); pong.maximumPlayerCount(pingInfo.getPlayers().getMax()); } else { pong.playerCount(geyser.getSessionManager().getSessions().size()); - pong.maximumPlayerCount(config.getMaxPlayers()); + pong.maximumPlayerCount(config.maxPlayers()); } this.geyser.eventBus().fire(new GeyserBedrockPingEventImpl(pong, inetSocketAddress)); @@ -397,6 +401,35 @@ private static String pingVersion() { return version; } + private List whitelistedIPsMatchers = null; + + /** + * @return Unmodifiable list of {@link CIDRMatcher}s from {@link GeyserConfig.BedrockConfig#proxyProtocolWhitelistedIps()} + */ + public List getWhitelistedIPsMatchers() { + // Effective Java, Third Edition; Item 83: Use lazy initialization judiciously + List matchers = this.whitelistedIPsMatchers; + if (matchers == null) { + synchronized (this) { + // Check if proxyProtocolWhitelistedIPs contains URLs we need to fetch and parse by line + List whitelistedCIDRs = new ArrayList<>(); + for (String ip: geyser.config().bedrock().proxyProtocolWhitelistedIps()) { + if (!ip.startsWith("http")) { + whitelistedCIDRs.add(ip); + continue; + } + + WebUtils.getLineStream(ip).forEach(whitelistedCIDRs::add); + } + + this.whitelistedIPsMatchers = matchers = whitelistedCIDRs.stream() + .map(CIDRMatcher::new) + .collect(Collectors.toList()); + } + } + return Collections.unmodifiableList(matchers); + } + /** * @return the throwable from the given supplier, or the throwable caught while calling the supplier. */ diff --git a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java index 25a0f0ee044..582744c8399 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java @@ -25,27 +25,29 @@ package org.geysermc.geyser.pack; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.pack.ResourcePackManifest; -import java.io.IOException; +import java.lang.reflect.Type; import java.util.Collection; import java.util.UUID; -public record GeyserResourcePackManifest(@JsonProperty("format_version") int formatVersion, Header header, Collection modules, Collection dependencies) implements ResourcePackManifest { +public record GeyserResourcePackManifest(@SerializedName("format_version") int formatVersion, Header header, Collection modules, Collection dependencies) implements ResourcePackManifest { - public record Header(UUID uuid, Version version, String name, String description, @JsonProperty("min_engine_version") Version minimumSupportedMinecraftVersion) implements ResourcePackManifest.Header { } + public record Header(UUID uuid, Version version, String name, String description, @SerializedName("min_engine_version") Version minimumSupportedMinecraftVersion) implements ResourcePackManifest.Header { } public record Module(UUID uuid, Version version, String type, String description) implements ResourcePackManifest.Module { } public record Dependency(UUID uuid, Version version) implements ResourcePackManifest.Dependency { } - @JsonDeserialize(using = Version.VersionDeserializer.class) + @JsonAdapter(value = Version.VersionDeserializer.class) public record Version(int major, int minor, int patch) implements ResourcePackManifest.Version { @Override @@ -53,11 +55,11 @@ public record Version(int major, int minor, int patch) implements ResourcePackMa return major + "." + minor + "." + patch; } - public static class VersionDeserializer extends JsonDeserializer { + public static class VersionDeserializer implements JsonDeserializer { @Override - public Version deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - int[] version = ctxt.readValue(p, int[].class); - return new Version(version[0], version[1], version[2]); + public Version deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonArray array = json.getAsJsonArray(); + return new Version(array.get(0).getAsInt(), array.get(1).getAsInt(), array.get(2).getAsInt()); } } } diff --git a/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java b/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java index 59651d1393a..03cdc60b27f 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java +++ b/core/src/main/java/org/geysermc/geyser/pack/SkullResourcePackManager.java @@ -78,7 +78,7 @@ public class SkullResourcePackManager { Path packPath = cachePath.resolve("player_skulls.mcpack"); File packFile = packPath.toFile(); - if (BlockRegistries.CUSTOM_SKULLS.get().isEmpty() || !GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + if (BlockRegistries.CUSTOM_SKULLS.get().isEmpty() || !GeyserImpl.getInstance().config().addNonBedrockItems()) { packFile.delete(); // No need to keep resource pack return null; } @@ -161,7 +161,7 @@ public static void cleanSkullSkinCache() { } } catch (IOException e) { GeyserImpl.getInstance().getLogger().debug("Unable to clean up skull skin cache."); - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { e.printStackTrace(); } } diff --git a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java index 27b40534838..5ceeb1651a5 100644 --- a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java +++ b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java @@ -25,8 +25,7 @@ package org.geysermc.geyser.ping; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; +import com.google.gson.JsonSyntaxException; import io.netty.handler.codec.haproxy.HAProxyCommand; import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; import io.netty.util.NetUtil; @@ -34,9 +33,19 @@ import org.cloudburstmc.nbt.util.VarInts; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.network.GameProtocol; - -import java.io.*; -import java.net.*; +import org.geysermc.geyser.util.JsonUtils; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.ConnectException; +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import java.util.concurrent.TimeUnit; public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runnable { @@ -56,10 +65,10 @@ public GeyserLegacyPingPassthrough(GeyserImpl geyser) { * @return GeyserPingPassthrough, or null if not initialized */ public static @Nullable IGeyserPingPassthrough init(GeyserImpl geyser) { - if (geyser.getConfig().isPassthroughMotd() || geyser.getConfig().isPassthroughPlayerCounts()) { + if (geyser.config().passthroughMotd() || geyser.config().passthroughPlayerCounts()) { GeyserLegacyPingPassthrough pingPassthrough = new GeyserLegacyPingPassthrough(geyser); // Ensure delay is not zero - int interval = (geyser.getConfig().getPingPassthroughInterval() == 0) ? 1 : geyser.getConfig().getPingPassthroughInterval(); + int interval = (geyser.config().pingPassthroughInterval() == 0) ? 1 : geyser.config().pingPassthroughInterval(); geyser.getLogger().debug("Scheduling ping passthrough at an interval of " + interval + " second(s)."); geyser.getScheduledThread().scheduleAtFixedRate(pingPassthrough, 1, interval, TimeUnit.SECONDS); return pingPassthrough; @@ -75,8 +84,8 @@ public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) { @Override public void run() { try (Socket socket = new Socket()) { - String address = geyser.getConfig().getRemote().address(); - int port = geyser.getConfig().getRemote().port(); + String address = geyser.config().java().address(); + int port = geyser.config().java().port(); InetSocketAddress endpoint = new InetSocketAddress(address, port); socket.connect(endpoint, 5000); @@ -93,7 +102,7 @@ public void run() { byte[] buffer; try (DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream())) { - if (geyser.getConfig().getRemote().isUseProxyProtocol()) { + if (geyser.config().java().useProxyProtocol()) { // HAProxy support // Based on https://github.com/netty/netty/blob/d8ad931488f6b942dabe28ecd6c399b4438da0a8/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageEncoder.java#L78 dataOutputStream.write(HAPROXY_BINARY_PREFIX); @@ -130,11 +139,11 @@ public void run() { } } - this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(buffer, GeyserPingInfo.class); + this.pingInfo = JsonUtils.fromJson(buffer, GeyserPingInfo.class); } catch (SocketTimeoutException | ConnectException ex) { this.pingInfo = null; this.geyser.getLogger().debug("Connection timeout for ping passthrough."); - } catch (JsonParseException | JsonMappingException ex) { + } catch (JsonSyntaxException ex) { this.geyser.getLogger().error("Failed to parse json when pinging server!", ex); } catch (EOFException e) { this.pingInfo = null; diff --git a/core/src/main/java/org/geysermc/geyser/ping/GeyserPingInfo.java b/core/src/main/java/org/geysermc/geyser/ping/GeyserPingInfo.java index 9d8da114d1c..853fa6187d7 100644 --- a/core/src/main/java/org/geysermc/geyser/ping/GeyserPingInfo.java +++ b/core/src/main/java/org/geysermc/geyser/ping/GeyserPingInfo.java @@ -25,21 +25,25 @@ package org.geysermc.geyser.ping; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.JsonAdapter; import lombok.Data; import org.checkerframework.checker.nullness.qual.Nullable; +import java.lang.reflect.Type; + /** * The structure of this class and its nested classes are specifically * designed for the format received by {@link GeyserLegacyPingPassthrough}. */ @Data -@JsonIgnoreProperties(ignoreUnknown = true) public class GeyserPingInfo { @Nullable + @JsonAdapter(DescriptionDeserializer.class) private String description; private Players players; @@ -58,13 +62,7 @@ public GeyserPingInfo(@Nullable String description, int maxPlayers, int onlinePl this.players = new Players(maxPlayers, onlinePlayers); } - @JsonSetter("description") - void setDescription(JsonNode description) { - this.description = description.toString(); - } - @Data - @JsonIgnoreProperties(ignoreUnknown = true) public static class Players { private int max; @@ -79,4 +77,14 @@ public Players(int max, int online) { this.online = online; } } + + /** + * So GSON does not complain how we are treating Description - it will be converted to a proper Component later. + */ + private static final class DescriptionDeserializer implements JsonDeserializer { + @Override + public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return json.toString(); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java b/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java index b31f2b4f000..1fc32f5e79f 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java @@ -71,7 +71,7 @@ public

boolean translate(Class clazz, P packet, Geyse } return true; } else { - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { if (!IGNORED_PACKETS.contains(clazz)) { GeyserImpl.getInstance().getLogger().debug("Could not find packet for " + (packet.toString().length() > 25 ? packet.getClass().getSimpleName() : packet)); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/BiomeIdentifierRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/BiomeIdentifierRegistryLoader.java index 3205004e438..78d7296057f 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/BiomeIdentifierRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/BiomeIdentifierRegistryLoader.java @@ -25,14 +25,16 @@ package org.geysermc.geyser.registry.loader; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.type.TypeReference; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.util.JsonUtils; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Type; import java.util.Map; public class BiomeIdentifierRegistryLoader implements RegistryLoader> { @@ -44,11 +46,11 @@ public Object2IntMap load(String input) { // The server sends the corresponding Java network IDs, so we don't need to worry about that now. // Reference variable for Jackson to read off of - TypeReference> biomeEntriesType = new TypeReference<>() { }; + Type biomeEntriesType = new TypeToken>() { }.getType(); Map biomeEntries; try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("mappings/biomes.json")) { - biomeEntries = GeyserImpl.JSON_MAPPER.readValue(stream, biomeEntriesType); + biomeEntries = JsonUtils.fromJson(stream, biomeEntriesType); } catch (IOException e) { throw new AssertionError("Unable to load Bedrock runtime biomes", e); } @@ -66,7 +68,7 @@ private static class BiomeEntry { /** * The Bedrock network ID for this biome. */ - @JsonProperty("bedrock_id") + @SerializedName("bedrock_id") private int bedrockId; } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/EffectRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/EffectRegistryLoader.java index aa95c1d563c..e6d8df39bd0 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/EffectRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/EffectRegistryLoader.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.registry.loader; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.geysermc.geyser.GeyserImpl; import java.io.InputStream; -import java.util.Map; -import java.util.WeakHashMap; +import java.io.InputStreamReader; /** * An abstract registry loader for loading effects from a resource path. @@ -38,21 +38,12 @@ * @param the value */ public abstract class EffectRegistryLoader implements RegistryLoader { - private static final Map loadedFiles = new WeakHashMap<>(); - public void loadFile(String input) { - if (!loadedFiles.containsKey(input)) { - JsonNode effects; - try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(input)) { - effects = GeyserImpl.JSON_MAPPER.readTree(stream); - } catch (Exception e) { - throw new AssertionError("Unable to load registrations for " + input, e); - } - loadedFiles.put(input, effects); + public JsonObject loadFile(String input) { + try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(input)) { + return new JsonParser().parse(new InputStreamReader(stream)).getAsJsonObject(); + } catch (Exception e) { + throw new AssertionError("Unable to load registrations for " + input, e); } } - - public JsonNode get(String input) { - return loadedFiles.get(input); - } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ParticleTypesRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ParticleTypesRegistryLoader.java index a09d6d6d3a1..42e34a2d59b 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ParticleTypesRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ParticleTypesRegistryLoader.java @@ -25,15 +25,15 @@ package org.geysermc.geyser.registry.loader; -import com.fasterxml.jackson.databind.JsonNode; -import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.LevelEventType; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.registry.type.ParticleMapping; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; -import java.util.Iterator; import java.util.Locale; import java.util.Map; @@ -44,16 +44,13 @@ public class ParticleTypesRegistryLoader extends EffectRegistryLoader load(String input) { - this.loadFile(input); - - Iterator> particlesIterator = this.get(input).fields(); + JsonObject particlesJson = this.loadFile(input); Map particles = new Object2ObjectOpenHashMap<>(); try { - while (particlesIterator.hasNext()) { - Map.Entry entry = particlesIterator.next(); + for (Map.Entry entry : particlesJson.entrySet()) { String key = entry.getKey().toUpperCase(Locale.ROOT); - JsonNode bedrockId = entry.getValue().get("bedrockId"); - JsonNode eventType = entry.getValue().get("eventType"); + JsonElement bedrockId = entry.getValue().getAsJsonObject().get("bedrockId"); + JsonElement eventType = entry.getValue().getAsJsonObject().get("eventType"); if (eventType == null && bedrockId == null) { GeyserImpl.getInstance().getLogger().debug("Skipping particle mapping " + key + " because no Bedrock equivalent exists."); continue; @@ -63,16 +60,16 @@ public Map load(String input) { if (eventType != null) { try { // Check if we have a particle type mapping - type = org.cloudburstmc.protocol.bedrock.data.ParticleType.valueOf(eventType.asText().toUpperCase(Locale.ROOT)); + type = org.cloudburstmc.protocol.bedrock.data.ParticleType.valueOf(eventType.getAsString().toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ex) { // No particle type; try level event - type = LevelEvent.valueOf(eventType.asText().toUpperCase(Locale.ROOT)); + type = LevelEvent.valueOf(eventType.getAsString().toUpperCase(Locale.ROOT)); } } particles.put(ParticleType.valueOf(key), new ParticleMapping( type, - bedrockId == null ? null : bedrockId.asText()) + bedrockId == null ? null : bedrockId.getAsString()) ); } } catch (Exception e) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/SoundEventsRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/SoundEventsRegistryLoader.java index 4e3fbe40a51..441a74f48aa 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/SoundEventsRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/SoundEventsRegistryLoader.java @@ -25,8 +25,8 @@ package org.geysermc.geyser.registry.loader; -import com.fasterxml.jackson.databind.JsonNode; -import org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEvent; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.cloudburstmc.protocol.bedrock.data.LevelEventType; import org.geysermc.geyser.GeyserImpl; @@ -34,8 +34,8 @@ import org.geysermc.geyser.translator.level.event.PlaySoundEventTranslator; import org.geysermc.geyser.translator.level.event.SoundEventEventTranslator; import org.geysermc.geyser.translator.level.event.SoundLevelEventTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEvent; -import java.util.Iterator; import java.util.Map; /** @@ -47,37 +47,36 @@ public class SoundEventsRegistryLoader extends EffectRegistryLoader load(String input) { this.loadFile(input); - Iterator> effectsIterator = this.get(input).fields(); + JsonObject effectsJson = this.loadFile(input); Map soundEffects = new Object2ObjectOpenHashMap<>(); - while (effectsIterator.hasNext()) { - Map.Entry entry = effectsIterator.next(); - JsonNode node = entry.getValue(); + for (Map.Entry entry : effectsJson.entrySet()) { + JsonObject node = entry.getValue().getAsJsonObject(); try { - String type = node.get("type").asText(); + String type = node.get("type").getAsString(); LevelEvent javaEffect = null; LevelEventTranslator transformer = null; switch (type) { case "soundLevel" -> { javaEffect = org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEventType.valueOf(entry.getKey()); - LevelEventType levelEventType = org.cloudburstmc.protocol.bedrock.data.LevelEvent.valueOf(node.get("name").asText()); - int data = node.has("data") ? node.get("data").intValue() : 0; + LevelEventType levelEventType = org.cloudburstmc.protocol.bedrock.data.LevelEvent.valueOf(node.get("name").getAsString()); + int data = node.has("data") ? node.get("data").getAsInt() : 0; transformer = new SoundLevelEventTranslator(levelEventType, data); } case "soundEvent" -> { javaEffect = org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEventType.valueOf(entry.getKey()); - org.cloudburstmc.protocol.bedrock.data.SoundEvent soundEvent = org.cloudburstmc.protocol.bedrock.data.SoundEvent.valueOf(node.get("name").asText()); - String identifier = node.has("identifier") ? node.get("identifier").asText() : ""; - int extraData = node.has("extraData") ? node.get("extraData").intValue() : -1; + org.cloudburstmc.protocol.bedrock.data.SoundEvent soundEvent = org.cloudburstmc.protocol.bedrock.data.SoundEvent.valueOf(node.get("name").getAsString()); + String identifier = node.has("identifier") ? node.get("identifier").getAsString() : ""; + int extraData = node.has("extraData") ? node.get("extraData").getAsInt() : -1; transformer = new SoundEventEventTranslator(soundEvent, identifier, extraData); } case "playSound" -> { javaEffect = org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEventType.valueOf(entry.getKey()); - String name = node.get("name").asText(); - float volume = node.has("volume") ? node.get("volume").floatValue() : 1.0f; - boolean pitchSub = node.has("pitch_sub") && node.get("pitch_sub").booleanValue(); - float pitchMul = node.has("pitch_mul") ? node.get("pitch_mul").floatValue() : 1.0f; - float pitchAdd = node.has("pitch_add") ? node.get("pitch_add").floatValue() : 0.0f; - boolean relative = !node.has("relative") || node.get("relative").booleanValue(); + String name = node.get("name").getAsString(); + float volume = node.has("volume") ? node.get("volume").getAsFloat() : 1.0f; + boolean pitchSub = node.has("pitch_sub") && node.get("pitch_sub").getAsBoolean(); + float pitchMul = node.has("pitch_mul") ? node.get("pitch_mul").getAsFloat() : 1.0f; + float pitchAdd = node.has("pitch_add") ? node.get("pitch_add").getAsFloat() : 0.0f; + boolean relative = !node.has("relative") || node.get("relative").getAsBoolean(); transformer = new PlaySoundEventTranslator(name, volume, pitchSub, pitchMul, pitchAdd, relative); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java index 318cc08d704..1212a9a1888 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java @@ -25,14 +25,16 @@ package org.geysermc.geyser.registry.loader; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.registry.type.SoundMapping; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; /** @@ -42,26 +44,24 @@ public class SoundRegistryLoader implements RegistryLoader load(String input) { - JsonNode soundsTree; + JsonObject soundsJson; try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(input)) { - soundsTree = GeyserImpl.JSON_MAPPER.readTree(stream); + soundsJson = new JsonParser().parse(new InputStreamReader(stream)).getAsJsonObject(); } catch (IOException e) { throw new AssertionError("Unable to load sound mappings", e); } Map soundMappings = new HashMap<>(); - Iterator> soundsIterator = soundsTree.fields(); - while(soundsIterator.hasNext()) { - Map.Entry next = soundsIterator.next(); - JsonNode brMap = next.getValue(); - String javaSound = next.getKey(); + for (Map.Entry entry : soundsJson.entrySet()) { + JsonObject brMap = entry.getValue().getAsJsonObject(); + String javaSound = entry.getKey(); soundMappings.put(javaSound, new SoundMapping( javaSound, - brMap.has("bedrock_mapping") && brMap.get("bedrock_mapping").isTextual() ? brMap.get("bedrock_mapping").asText() : null, - brMap.has("playsound_mapping") && brMap.get("playsound_mapping").isTextual() ? brMap.get("playsound_mapping").asText() : null, - brMap.has("extra_data") && brMap.get("extra_data").isInt() ? brMap.get("extra_data").asInt() : -1, - brMap.has("identifier") && brMap.get("identifier").isTextual() ? brMap.get("identifier").asText() : null, - brMap.has("level_event") && brMap.get("level_event").isBoolean() && brMap.get("level_event").asBoolean() + brMap.has("bedrock_mapping") ? brMap.get("bedrock_mapping").getAsString() : null, + brMap.has("playsound_mapping") ? brMap.get("playsound_mapping").getAsString() : null, + brMap.has("extra_data") ? brMap.get("extra_data").getAsInt() : -1, + brMap.has("identifier") ? brMap.get("identifier").getAsString() : null, + brMap.has("level_event") && brMap.get("level_event").getAsBoolean() ) ); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java index d09e0b5a1e1..6fd23c73852 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/MappingsConfigReader.java @@ -25,7 +25,8 @@ package org.geysermc.geyser.registry.mappings; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,6 +36,7 @@ import org.geysermc.geyser.registry.mappings.versions.MappingsReader; import org.geysermc.geyser.registry.mappings.versions.MappingsReader_v1; +import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -95,10 +97,10 @@ public void loadBlockMappingsFromJson(BiConsumer con } } - public @Nullable JsonNode getMappingsRoot(Path file) { - JsonNode mappingsRoot; - try { - mappingsRoot = GeyserImpl.JSON_MAPPER.readTree(file.toFile()); + public @Nullable JsonObject getMappingsRoot(Path file) { + JsonObject mappingsRoot; + try (FileReader reader = new FileReader(file.toFile())) { + mappingsRoot = (JsonObject) new JsonParser().parse(reader); } catch (IOException e) { GeyserImpl.getInstance().getLogger().error("Failed to read custom mapping file: " + file, e); return null; @@ -112,8 +114,8 @@ public void loadBlockMappingsFromJson(BiConsumer con return mappingsRoot; } - public int getFormatVersion(JsonNode mappingsRoot, Path file) { - int formatVersion = mappingsRoot.get("format_version").asInt(); + public int getFormatVersion(JsonObject mappingsRoot, Path file) { + int formatVersion = mappingsRoot.get("format_version").getAsInt(); if (!this.mappingReaders.containsKey(formatVersion)) { GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " has an unknown format version: " + formatVersion); return -1; @@ -122,7 +124,7 @@ public int getFormatVersion(JsonNode mappingsRoot, Path file) { } public void readItemMappingsFromJson(Path file, BiConsumer consumer) { - JsonNode mappingsRoot = getMappingsRoot(file); + JsonObject mappingsRoot = getMappingsRoot(file); if (mappingsRoot == null) { return; @@ -138,7 +140,7 @@ public void readItemMappingsFromJson(Path file, BiConsumer consumer) { - JsonNode mappingsRoot = getMappingsRoot(file); + JsonObject mappingsRoot = getMappingsRoot(file); if (mappingsRoot == null) { return; diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java index b2bdd5a0130..43d9a23d999 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.registry.mappings.versions; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; @@ -36,14 +36,14 @@ import java.util.function.BiConsumer; public abstract class MappingsReader { - public abstract void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); - public abstract void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer); + public abstract void readItemMappings(Path file, JsonObject mappingsRoot, BiConsumer consumer); + public abstract void readBlockMappings(Path file, JsonObject mappingsRoot, BiConsumer consumer); - public abstract CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException; - public abstract CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException; + public abstract CustomItemData readItemMappingEntry(JsonObject node) throws InvalidCustomMappingsFileException; + public abstract CustomBlockMapping readBlockMappingEntry(String identifier, JsonObject node) throws InvalidCustomMappingsFileException; - protected @Nullable CustomRenderOffsets fromJsonNode(JsonNode node) { - if (node == null || !node.isObject()) { + protected @Nullable CustomRenderOffsets fromJsonObject(JsonObject node) { + if (node == null) { return null; } @@ -53,9 +53,8 @@ public abstract class MappingsReader { ); } - protected CustomRenderOffsets.@Nullable Hand getHandOffsets(JsonNode node, String hand) { - JsonNode tmpNode = node.get(hand); - if (tmpNode == null || !tmpNode.isObject()) { + protected CustomRenderOffsets.@Nullable Hand getHandOffsets(JsonObject node, String hand) { + if (!(node.get(hand) instanceof JsonObject tmpNode)) { return null; } @@ -65,9 +64,8 @@ public abstract class MappingsReader { ); } - protected CustomRenderOffsets.@Nullable Offset getPerspectiveOffsets(JsonNode node, String perspective) { - JsonNode tmpNode = node.get(perspective); - if (tmpNode == null || !tmpNode.isObject()) { + protected CustomRenderOffsets.@Nullable Offset getPerspectiveOffsets(JsonObject node, String perspective) { + if (!(node.get(perspective) instanceof JsonObject tmpNode)) { return null; } @@ -78,9 +76,8 @@ public abstract class MappingsReader { ); } - protected CustomRenderOffsets.@Nullable OffsetXYZ getOffsetXYZ(JsonNode node, String offsetType) { - JsonNode tmpNode = node.get(offsetType); - if (tmpNode == null || !tmpNode.isObject()) { + protected CustomRenderOffsets.@Nullable OffsetXYZ getOffsetXYZ(JsonObject node, String offsetType) { + if (!(node.get(offsetType) instanceof JsonObject tmpNode)) { return null; } @@ -89,9 +86,9 @@ public abstract class MappingsReader { } return new CustomRenderOffsets.OffsetXYZ( - tmpNode.get("x").floatValue(), - tmpNode.get("y").floatValue(), - tmpNode.get("z").floatValue() + tmpNode.get("x").getAsFloat(), + tmpNode.get("y").getAsFloat(), + tmpNode.get("z").getAsFloat() ); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java index b5e25a4ba5c..bbde78d0377 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v1.java @@ -25,8 +25,10 @@ package org.geysermc.geyser.registry.mappings.versions; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import org.checkerframework.checker.nullness.qual.Nullable; @@ -34,9 +36,14 @@ import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.api.block.custom.CustomBlockPermutation; import org.geysermc.geyser.api.block.custom.CustomBlockState; -import org.geysermc.geyser.api.block.custom.component.*; +import org.geysermc.geyser.api.block.custom.component.BoxComponent; +import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents; +import org.geysermc.geyser.api.block.custom.component.GeometryComponent; +import org.geysermc.geyser.api.block.custom.component.MaterialInstance; +import org.geysermc.geyser.api.block.custom.component.PlacementConditions; import org.geysermc.geyser.api.block.custom.component.PlacementConditions.BlockFilterType; import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face; +import org.geysermc.geyser.api.block.custom.component.TransformationComponent; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.util.CreativeCategory; @@ -57,7 +64,13 @@ import org.geysermc.geyser.util.MinecraftKey; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -68,7 +81,7 @@ */ public class MappingsReader_v1 extends MappingsReader { @Override - public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + public void readItemMappings(Path file, JsonObject mappingsRoot, BiConsumer consumer) { this.readItemMappingsV1(file, mappingsRoot, consumer); } @@ -76,24 +89,24 @@ public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer consumer) { + public void readBlockMappings(Path file, JsonObject mappingsRoot, BiConsumer consumer) { this.readBlockMappingsV1(file, mappingsRoot, consumer); } - public void readItemMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer consumer) { - JsonNode itemsNode = mappingsRoot.get("items"); + public void readItemMappingsV1(Path file, JsonObject mappingsRoot, BiConsumer consumer) { + JsonObject itemsNode = mappingsRoot.getAsJsonObject("items"); - if (itemsNode != null && itemsNode.isObject()) { - itemsNode.fields().forEachRemaining(entry -> { - if (entry.getValue().isArray()) { - entry.getValue().forEach(data -> { + if (itemsNode != null) { + itemsNode.entrySet().forEach(entry -> { + if (entry.getValue() instanceof JsonArray array) { + array.forEach(data -> { try { - CustomItemData customItemData = this.readItemMappingEntry(data); + CustomItemData customItemData = this.readItemMappingEntry((JsonObject) data); consumer.accept(entry.getKey(), customItemData); } catch (InvalidCustomMappingsFileException e) { GeyserImpl.getInstance().getLogger().error("Error in registering items for custom mapping file: " + file.toString(), e); @@ -108,19 +121,17 @@ public void readItemMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer consumer) { - JsonNode blocksNode = mappingsRoot.get("blocks"); - - if (blocksNode != null && blocksNode.isObject()) { - blocksNode.fields().forEachRemaining(entry -> { - if (entry.getValue().isObject()) { + public void readBlockMappingsV1(Path file, JsonObject mappingsRoot, BiConsumer consumer) { + if (mappingsRoot.get("blocks") instanceof JsonObject blocksNode) { + blocksNode.entrySet().forEach(entry -> { + if (entry.getValue() instanceof JsonObject jsonObject) { try { String identifier = MinecraftKey.key(entry.getKey()).asString(); - CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, entry.getValue()); + CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, jsonObject); consumer.accept(identifier, customBlockMapping); } catch (Exception e) { GeyserImpl.getInstance().getLogger().error("Error in registering blocks for custom mapping file: " + file.toString()); @@ -131,85 +142,85 @@ public void readBlockMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer tagsSet = new ObjectOpenHashSet<>(); - tags.forEach(tag -> tagsSet.add(tag.asText())); + tags.forEach(tag -> tagsSet.add(tag.getAsString())); customItemData.tags(tagsSet); } @@ -220,26 +231,26 @@ public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMa * Read a block mapping entry from a JSON node and Java identifier * * @param identifier The Java identifier of the block - * @param node The {@link JsonNode} containing the block mapping entry + * @param node The {@link JsonObject} containing the block mapping entry * @return The {@link CustomBlockMapping} record to be read by {@link org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator} * @throws InvalidCustomMappingsFileException If the JSON node is invalid */ @Override - public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException { - if (node == null || !node.isObject()) { + public CustomBlockMapping readBlockMappingEntry(String identifier, JsonObject node) throws InvalidCustomMappingsFileException { + if (node == null) { throw new InvalidCustomMappingsFileException("Invalid block mappings entry:" + node); } - String name = node.get("name").asText(); + String name = node.get("name").getAsString(); if (name == null || name.isEmpty()) { throw new InvalidCustomMappingsFileException("A block entry has no name"); } - boolean includedInCreativeInventory = node.has("included_in_creative_inventory") && node.get("included_in_creative_inventory").asBoolean(); + boolean includedInCreativeInventory = node.has("included_in_creative_inventory") && node.get("included_in_creative_inventory").getAsBoolean(); CreativeCategory creativeCategory = CreativeCategory.NONE; if (node.has("creative_category")) { - String categoryName = node.get("creative_category").asText(); + String categoryName = node.get("creative_category").getAsString(); try { creativeCategory = CreativeCategory.valueOf(categoryName.toUpperCase()); } catch (IllegalArgumentException e) { @@ -249,11 +260,11 @@ public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node String creativeGroup = ""; if (node.has("creative_group")) { - creativeGroup = node.get("creative_group").asText(); + creativeGroup = node.get("creative_group").getAsString(); } // If this is true, we will only register the states the user has specified rather than all the possible block states - boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").asBoolean(); + boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").getAsBoolean(); // Create the data for the overall block CustomBlockData.Builder customBlockDataBuilder = new GeyserCustomBlockData.Builder() @@ -273,12 +284,9 @@ public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node Map componentsMap = new LinkedHashMap<>(); - JsonNode stateOverrides = node.get("state_overrides"); - if (stateOverrides != null && stateOverrides.isObject()) { + if (node.get("state_overrides") instanceof JsonObject stateOverrides) { // Load components for specific Java block states - Iterator> fields = stateOverrides.fields(); - while (fields.hasNext()) { - Map.Entry overrideEntry = fields.next(); + for (Map.Entry overrideEntry : stateOverrides.entrySet()) { String state = identifier + "[" + overrideEntry.getKey() + "]"; if (!BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().containsKey(state)) { throw new InvalidCustomMappingsFileException("Unknown Java block state: " + state + " for state_overrides."); @@ -358,12 +366,12 @@ private CustomBlockMapping createCustomBlockMapping(CustomBlockData.Builder cust /** * Creates a {@link CustomBlockComponents} object for the passed state override or base block node, Java block state identifier, and custom block name * - * @param node the state override or base block {@link JsonNode} + * @param element the state override or base block {@link JsonObject} * @param stateKey the Java block state identifier * @param name the name of the custom block * @return the {@link CustomBlockComponents} object */ - private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode node, String stateKey, String name) { + private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonElement element, String stateKey, String name) { // This is needed to find the correct selection box for the given block int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(stateKey, -1); BoxComponent boxComponent = createBoxComponent(id); @@ -372,7 +380,7 @@ private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode .collisionBox(boxComponent) .selectionBox(boxComponent); - if (node == null) { + if (!(element instanceof JsonObject node)) { // No other components were defined return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent); } @@ -394,28 +402,28 @@ private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode // We set this to max value by default so that we may dictate the correct destroy time ourselves float destructibleByMining = Float.MAX_VALUE; if (node.has("destructible_by_mining")) { - destructibleByMining = node.get("destructible_by_mining").floatValue(); + destructibleByMining = node.get("destructible_by_mining").getAsFloat(); } builder.destructibleByMining(destructibleByMining); if (node.has("geometry")) { - if (node.get("geometry").isTextual()) { + if (node.get("geometry").isJsonPrimitive()) { builder.geometry(new GeyserGeometryComponent.Builder() - .identifier(node.get("geometry").asText()) + .identifier(node.get("geometry").getAsString()) .build()); } else { - JsonNode geometry = node.get("geometry"); + JsonObject geometry = node.getAsJsonObject("geometry"); GeometryComponent.Builder geometryBuilder = new GeyserGeometryComponent.Builder(); if (geometry.has("identifier")) { - geometryBuilder.identifier(geometry.get("identifier").asText()); + geometryBuilder.identifier(geometry.get("identifier").getAsString()); } if (geometry.has("bone_visibility")) { - JsonNode boneVisibility = geometry.get("bone_visibility"); - if (boneVisibility.isObject()) { + if (geometry.get("bone_visibility") instanceof JsonObject boneVisibility) { Map boneVisibilityMap = new Object2ObjectOpenHashMap<>(); - boneVisibility.fields().forEachRemaining(entry -> { + boneVisibility.entrySet().forEach(entry -> { String key = entry.getKey(); - String value = entry.getValue().isBoolean() ? (entry.getValue().asBoolean() ? "1" : "0") : entry.getValue().asText(); + String value = entry.getValue() instanceof JsonPrimitive primitive && primitive.isBoolean() + ? (entry.getValue().getAsBoolean() ? "1" : "0") : entry.getValue().getAsString(); boneVisibilityMap.put(key, value); }); geometryBuilder.boneVisibility(boneVisibilityMap); @@ -427,30 +435,30 @@ private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode String displayName = name; if (node.has("display_name")) { - displayName = node.get("display_name").asText(); + displayName = node.get("display_name").getAsString(); } builder.displayName(displayName); if (node.has("friction")) { - builder.friction(node.get("friction").floatValue()); + builder.friction(node.get("friction").getAsFloat()); } if (node.has("light_emission")) { - builder.lightEmission(node.get("light_emission").asInt()); + builder.lightEmission(node.get("light_emission").getAsInt()); } if (node.has("light_dampening")) { - builder.lightDampening(node.get("light_dampening").asInt()); + builder.lightDampening(node.get("light_dampening").getAsInt()); } boolean placeAir = true; if (node.has("place_air")) { - placeAir = node.get("place_air").asBoolean(); + placeAir = node.get("place_air").getAsBoolean(); } builder.placeAir(placeAir); if (node.has("transformation")) { - JsonNode transformation = node.get("transformation"); + JsonObject transformation = node.getAsJsonObject("transformation"); int rotationX = 0; int rotationY = 0; @@ -463,22 +471,22 @@ private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode float transformZ = 0; if (transformation.has("rotation")) { - JsonNode rotation = transformation.get("rotation"); - rotationX = rotation.get(0).asInt(); - rotationY = rotation.get(1).asInt(); - rotationZ = rotation.get(2).asInt(); + JsonArray rotation = transformation.getAsJsonArray("rotation"); + rotationX = rotation.get(0).getAsInt(); + rotationY = rotation.get(1).getAsInt(); + rotationZ = rotation.get(2).getAsInt(); } if (transformation.has("scale")) { - JsonNode scale = transformation.get("scale"); - scaleX = scale.get(0).floatValue(); - scaleY = scale.get(1).floatValue(); - scaleZ = scale.get(2).floatValue(); + JsonArray scale = transformation.getAsJsonArray("scale"); + scaleX = scale.get(0).getAsFloat(); + scaleY = scale.get(1).getAsFloat(); + scaleZ = scale.get(2).getAsFloat(); } if (transformation.has("translation")) { - JsonNode translation = transformation.get("translation"); - transformX = translation.get(0).floatValue(); - transformY = translation.get(1).floatValue(); - transformZ = translation.get(2).floatValue(); + JsonArray translation = transformation.getAsJsonArray("translation"); + transformX = translation.get(0).getAsFloat(); + transformY = translation.get(1).getAsFloat(); + transformZ = translation.get(2).getAsFloat(); } builder.transformation(new TransformationComponent(rotationX, rotationY, rotationZ, scaleX, scaleY, scaleZ, transformX, transformY, transformZ)); } @@ -490,12 +498,10 @@ private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode } if (node.has("material_instances")) { - JsonNode materialInstances = node.get("material_instances"); - if (materialInstances.isObject()) { - materialInstances.fields().forEachRemaining(entry -> { + if (node.get("material_instances") instanceof JsonObject materialInstances) { + materialInstances.entrySet().forEach(entry -> { String key = entry.getKey(); - JsonNode value = entry.getValue(); - if (value.isObject()) { + if (entry.getValue() instanceof JsonObject value) { MaterialInstance materialInstance = createMaterialInstanceComponent(value); builder.materialInstance(key, materialInstance); } @@ -503,16 +509,10 @@ private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode } } - if (node.has("placement_filter")) { - JsonNode placementFilter = node.get("placement_filter"); - if (placementFilter.isObject()) { - if (placementFilter.has("conditions")) { - JsonNode conditions = placementFilter.get("conditions"); - if (conditions.isArray()) { - List filter = createPlacementFilterComponent(conditions); - builder.placementFilter(filter); - } - } + if (node.get("placement_filter") instanceof JsonObject placementFilter) { + if (placementFilter.get("conditions") instanceof JsonArray conditions) { + List filter = createPlacementFilterComponent(conditions); + builder.placementFilter(filter); } } @@ -521,9 +521,9 @@ private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode // Ideally we could programmatically extract the tags here https://wiki.bedrock.dev/blocks/block-tags.html // This would let us automatically apply the correct vanilla tags to blocks // However, its worth noting that vanilla tools do not currently honor these tags anyway - if (node.get("tags") instanceof ArrayNode tags) { + if (node.get("tags") instanceof JsonArray tags) { Set tagsSet = new ObjectOpenHashSet<>(); - tags.forEach(tag -> tagsSet.add(tag.asText())); + tags.forEach(tag -> tagsSet.add(tag.getAsString())); builder.tags(tagsSet); } @@ -613,21 +613,21 @@ private BoxComponent createBoxComponent(int javaId) { /** * Creates a {@link BoxComponent} from a JSON Node * - * @param node the JSON node + * @param element the JSON node * @return the {@link BoxComponent} */ - private @Nullable BoxComponent createBoxComponent(JsonNode node) { - if (node != null && node.isObject()) { + private @Nullable BoxComponent createBoxComponent(JsonElement element) { + if (element instanceof JsonObject node) { if (node.has("origin") && node.has("size")) { - JsonNode origin = node.get("origin"); - float originX = origin.get(0).floatValue(); - float originY = origin.get(1).floatValue(); - float originZ = origin.get(2).floatValue(); + JsonArray origin = node.getAsJsonArray("origin"); + float originX = origin.get(0).getAsFloat(); + float originY = origin.get(1).getAsFloat(); + float originZ = origin.get(2).getAsFloat(); - JsonNode size = node.get("size"); - float sizeX = size.get(0).floatValue(); - float sizeY = size.get(1).floatValue(); - float sizeZ = size.get(2).floatValue(); + JsonArray size = node.getAsJsonArray("size"); + float sizeX = size.get(0).getAsFloat(); + float sizeY = size.get(1).getAsFloat(); + float sizeZ = size.get(2).getAsFloat(); return new BoxComponent(originX, originY, originZ, sizeX, sizeY, sizeZ); } @@ -642,26 +642,26 @@ private BoxComponent createBoxComponent(int javaId) { * @param node the material instance node * @return the {@link MaterialInstance} */ - private MaterialInstance createMaterialInstanceComponent(JsonNode node) { + private MaterialInstance createMaterialInstanceComponent(JsonObject node) { // Set default values, and use what the user provides if they have provided something String texture = null; if (node.has("texture")) { - texture = node.get("texture").asText(); + texture = node.get("texture").getAsString(); } String renderMethod = "opaque"; if (node.has("render_method")) { - renderMethod = node.get("render_method").asText(); + renderMethod = node.get("render_method").getAsString(); } boolean faceDimming = true; if (node.has("face_dimming")) { - faceDimming = node.get("face_dimming").asBoolean(); + faceDimming = node.get("face_dimming").getAsBoolean(); } boolean ambientOcclusion = true; if (node.has("ambient_occlusion")) { - ambientOcclusion = node.get("ambient_occlusion").asBoolean(); + ambientOcclusion = node.get("ambient_occlusion").getAsBoolean(); } return new GeyserMaterialInstance.Builder() @@ -678,32 +678,33 @@ private MaterialInstance createMaterialInstanceComponent(JsonNode node) { * @param node the conditions node * @return the list of {@link PlacementConditions} */ - private List createPlacementFilterComponent(JsonNode node) { + private List createPlacementFilterComponent(JsonArray node) { List conditions = new ArrayList<>(); // The structure of the placement filter component is the most complex of the current components // Each condition effectively separated into two arrays: one of allowed faces, and one of blocks/block Molang queries - node.forEach(condition -> { + node.forEach(json -> { + if (!(json instanceof JsonObject condition)) { + return; + } Set faces = EnumSet.noneOf(Face.class); if (condition.has("allowed_faces")) { - JsonNode allowedFaces = condition.get("allowed_faces"); - if (allowedFaces.isArray()) { - allowedFaces.forEach(face -> faces.add(Face.valueOf(face.asText().toUpperCase()))); + if (condition.get("allowed_faces") instanceof JsonArray allowedFaces) { + allowedFaces.forEach(face -> faces.add(Face.valueOf(face.getAsString().toUpperCase()))); } } LinkedHashMap blockFilters = new LinkedHashMap<>(); if (condition.has("block_filter")) { - JsonNode blockFilter = condition.get("block_filter"); - if (blockFilter.isArray()) { + if (condition.get("block_filter") instanceof JsonArray blockFilter) { blockFilter.forEach(filter -> { - if (filter.isObject()) { - if (filter.has("tags")) { - JsonNode tags = filter.get("tags"); - blockFilters.put(tags.asText(), BlockFilterType.TAG); + if (filter instanceof JsonObject jsonObject) { + if (jsonObject.has("tags")) { + JsonElement tags = jsonObject.get("tags"); + blockFilters.put(tags.getAsString(), BlockFilterType.TAG); } - } else if (filter.isTextual()) { - blockFilters.put(filter.asText(), BlockFilterType.BLOCK); + } else if (filter instanceof JsonPrimitive primitive && primitive.isString()) { + blockFilters.put(filter.getAsString(), BlockFilterType.BLOCK); } }); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index 5b7213beec4..4ee6582d6f5 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -25,11 +25,12 @@ package org.geysermc.geyser.registry.populator; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Interner; import com.google.common.collect.Interners; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; @@ -68,6 +69,7 @@ import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.GeyserBedrockBlock; import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.geyser.util.JsonUtils; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.io.DataInputStream; @@ -501,23 +503,23 @@ public ItemStack pickItem(BlockState state) { BLOCKS_NBT = blocksNbt; - JsonNode blockInteractionsJson; + JsonObject blockInteractionsJson; try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("mappings/interactions.json")) { - blockInteractionsJson = GeyserImpl.JSON_MAPPER.readTree(stream); + blockInteractionsJson = JsonUtils.fromJson(stream); } catch (Exception e) { throw new AssertionError("Unable to load Java block interaction mappings", e); } - BlockRegistries.INTERACTIVE.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("always_consumes"))); - BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("requires_may_build"))); + BlockRegistries.INTERACTIVE.set(toBlockStateSet(blockInteractionsJson.getAsJsonArray("always_consumes"))); + BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet(blockInteractionsJson.getAsJsonArray("requires_may_build"))); BlockRegistries.BLOCK_STATES.freeze(); } - private static BitSet toBlockStateSet(ArrayNode node) { + private static BitSet toBlockStateSet(JsonArray node) { BitSet blockStateSet = new BitSet(node.size()); - for (JsonNode javaIdentifier : node) { - blockStateSet.set(BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getInt(javaIdentifier.textValue())); + for (JsonElement javaIdentifier : node) { + blockStateSet.set(BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getInt(javaIdentifier.getAsString())); } return blockStateSet; } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java index 8e42887ff0b..7d52a64cb79 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java @@ -25,7 +25,9 @@ package org.geysermc.geyser.registry.populator; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; @@ -37,6 +39,7 @@ import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.GeyserBedrockBlock; +import org.geysermc.geyser.util.JsonUtils; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -59,16 +62,16 @@ static void populate(ItemRegistryPopulator.PaletteVersion palette, Map definitions) { + private static ItemData.@Nullable Builder createItemData(JsonObject itemNode, BlockMappings blockMappings, Map definitions) { int count = 1; int damage = 0; NbtMap tag = null; - String identifier = itemNode.get("id").textValue(); + String identifier = itemNode.get("id").getAsString(); for (BiPredicate predicate : JAVA_ONLY_ITEM_FILTER) { if (predicate.test(identifier, damage)) { return null; } } - JsonNode damageNode = itemNode.get("damage"); + JsonElement damageNode = itemNode.get("damage"); if (damageNode != null) { - damage = damageNode.asInt(); + damage = damageNode.getAsInt(); } - JsonNode countNode = itemNode.get("count"); + JsonElement countNode = itemNode.get("count"); if (countNode != null) { - count = countNode.asInt(); + count = countNode.getAsInt(); } GeyserBedrockBlock blockDefinition = null; - JsonNode blockStateNode; + JsonElement blockStateNode; if ((blockStateNode = itemNode.get("block_state_b64")) != null) { - byte[] bytes = Base64.getDecoder().decode(blockStateNode.asText()); + byte[] bytes = Base64.getDecoder().decode(blockStateNode.getAsString()); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); try { NbtMap stateTag = (NbtMap) NbtUtils.createReaderLE(bais).readTag(); @@ -121,9 +124,9 @@ static void populate(ItemRegistryPopulator.PaletteVersion palette, Map> mappingItemsType = new TypeReference<>() { }; + Type mappingItemsType = new TypeToken>() { }.getType(); Map items; try (InputStream stream = bootstrap.getResourceOrThrow("mappings/items.json")) { // Load item mappings from Java Edition to Bedrock Edition - items = GeyserImpl.JSON_MAPPER.readValue(stream, mappingItemsType); + items = JsonUtils.fromJson(stream, mappingItemsType); } catch (Exception e) { throw new AssertionError("Unable to load Java runtime item IDs", e); } @@ -143,7 +145,7 @@ public static void populate() { throw new AssertionError("Unable to load Bedrock item components", e); } - boolean customItemsAllowed = GeyserImpl.getInstance().getConfig().isAddNonBedrockItems(); + boolean customItemsAllowed = GeyserImpl.getInstance().config().addNonBedrockItems(); // List values here is important compared to HashSet - we need to preserve the order of what's given to us // (as of 1.19.2 Java) to replicate some edge cases in Java predicate behavior where it checks from the bottom @@ -160,11 +162,11 @@ public static void populate() { /* Load item palette */ for (PaletteVersion palette : paletteVersions) { - TypeReference> paletteEntriesType = new TypeReference<>() {}; + Type paletteEntriesType = new TypeToken>() { }.getType(); List itemEntries; try (InputStream stream = bootstrap.getResourceOrThrow(String.format("bedrock/runtime_item_states.%s.json", palette.version()))) { - itemEntries = GeyserImpl.JSON_MAPPER.readValue(stream, paletteEntriesType); + itemEntries = JsonUtils.fromJson(stream, paletteEntriesType); } catch (Exception e) { throw new AssertionError("Unable to load Bedrock runtime item IDs", e); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java index ab8c52bf6f4..50198e0fca9 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.registry.type; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -43,14 +43,14 @@ @NoArgsConstructor @AllArgsConstructor public class GeyserMappingItem { - @JsonProperty("bedrock_identifier") String bedrockIdentifier; - @JsonProperty("bedrock_data") int bedrockData; + @SerializedName("bedrock_identifier") String bedrockIdentifier; + @SerializedName("bedrock_data") int bedrockData; Integer firstBlockRuntimeId; Integer lastBlockRuntimeId; - @JsonProperty("tool_type") String toolType; - @JsonProperty("tool_tier") String toolTier; - @JsonProperty("armor_type") String armorType; - @JsonProperty("protection_value") int protectionValue; - @JsonProperty("is_edible") boolean edible = false; - @JsonProperty("is_entity_placer") boolean entityPlacer = false; + @SerializedName("tool_type") String toolType; + @SerializedName("tool_tier") String toolTier; + @SerializedName("armor_type") String armorType; + @SerializedName("protection_value") int protectionValue; + @SerializedName("is_edible") boolean edible = false; + @SerializedName("is_entity_placer") boolean entityPlacer = false; } diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java b/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java index 18a4bce3947..19c5e64f0b8 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/ScoreboardUpdater.java @@ -28,7 +28,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.GeyserConfig; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.text.GeyserLocale; @@ -46,9 +46,9 @@ public final class ScoreboardUpdater extends Thread { private static final boolean DEBUG_ENABLED; static { - GeyserConfiguration config = GeyserImpl.getInstance().getConfig(); - FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD); - DEBUG_ENABLED = config.isDebugMode(); + GeyserConfig config = GeyserImpl.getInstance().config(); + FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.advanced().scoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD); + DEBUG_ENABLED = config.debugMode(); } private final GeyserImpl geyser = GeyserImpl.getInstance(); diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index ef6261ead56..cccd2aa056c 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -25,8 +25,6 @@ package org.geysermc.geyser.session; -import com.google.gson.Gson; -import com.google.gson.JsonObject; import io.netty.channel.Channel; import io.netty.channel.EventLoop; import it.unimi.dsi.fastutil.Pair; @@ -125,8 +123,7 @@ import org.geysermc.geyser.api.network.RemoteServer; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.GeyserConfig; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.GeyserEntityData; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; @@ -182,6 +179,7 @@ import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InventoryUtils; +import org.geysermc.geyser.util.JsonUtils; import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.mcprotocollib.auth.GameProfile; @@ -244,8 +242,6 @@ @Getter public class GeyserSession implements GeyserConnection, GeyserCommandSource { - private static final Gson GSON = new Gson(); - private final GeyserImpl geyser; private final UpstreamSession upstream; private DownstreamSession downstream; @@ -631,7 +627,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { /** * A cache of IDs from ClientboundKeepAlivePackets that have been sent to the Bedrock client, but haven't been returned to the server. - * Only used if {@link GeyserConfiguration#isForwardPlayerPing()} is enabled. + * Only used if {@link GeyserConfig#forwardPlayerPing()} is enabled. */ private final Queue keepAliveCache = new ConcurrentLinkedQueue<>(); @@ -694,12 +690,8 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio this.spawned = false; this.loggedIn = false; - if (geyser.getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.NO_EMOTES) { - this.emotes = new HashSet<>(); - geyser.getSessionManager().getSessions().values().forEach(player -> this.emotes.addAll(player.getEmotes())); - } else { - this.emotes = null; - } + this.emotes = new HashSet<>(); + geyser.getSessionManager().getSessions().values().forEach(player -> this.emotes.addAll(player.getEmotes())); this.remoteServer = geyser.defaultRemoteServer(); } @@ -737,7 +729,7 @@ public void connect() { sentSpawnPacket = true; syncEntityProperties(); - if (GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) { + if (GeyserImpl.getInstance().config().addNonBedrockItems()) { ItemComponentPacket componentPacket = new ItemComponentPacket(); componentPacket.getItems().addAll(itemMappings.getComponentItemData()); upstream.sendPacket(componentPacket); @@ -816,7 +808,7 @@ public void authenticateWithAuthChain(String authChain) { StepFullJavaSession step = PendingMicrosoftAuthentication.AUTH_FLOW.apply(true, 30); StepFullJavaSession.FullJavaSession response; try { - response = step.refresh(MinecraftAuthLogger.INSTANCE, PendingMicrosoftAuthentication.AUTH_CLIENT, step.fromJson(GSON.fromJson(authChain, JsonObject.class))); + response = step.refresh(MinecraftAuthLogger.INSTANCE, PendingMicrosoftAuthentication.AUTH_CLIENT, step.fromJson(JsonUtils.parseJson(authChain))); } catch (Exception e) { geyser.getLogger().error("Error while attempting to use auth chain for " + bedrockUsername() + "!", e); return Boolean.FALSE; @@ -829,7 +821,7 @@ public void authenticateWithAuthChain(String authChain) { new GameProfile(mcProfile.getId(), mcProfile.getName()), mcToken.getAccessToken() ); - geyser.saveAuthChain(bedrockUsername(), GSON.toJson(step.toJson(response))); + geyser.saveAuthChain(bedrockUsername(), GeyserImpl.GSON.toJson(step.toJson(response))); return Boolean.TRUE; }).whenComplete((successful, ex) -> { if (this.closed) { @@ -924,7 +916,7 @@ public boolean onMicrosoftLoginComplete(PendingMicrosoftAuthentication.Authentic } // Save our auth chain for later use - geyser.saveAuthChain(bedrockUsername(), GSON.toJson(result.step().toJson(result.session()))); + geyser.saveAuthChain(bedrockUsername(), GeyserImpl.GSON.toJson(result.step().toJson(result.session()))); return true; }).getNow(false); } @@ -973,10 +965,10 @@ private void connectDownstream() { // Disable automatic creation of a new TcpClientSession when transferring - we don't use that functionality. this.downstream.getSession().setFlag(MinecraftConstants.FOLLOW_TRANSFERS, false); - if (geyser.getConfig().getRemote().isUseProxyProtocol()) { + if (geyser.config().java().useProxyProtocol()) { downstream.setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); } - if (geyser.getConfig().isForwardPlayerPing()) { + if (geyser.config().forwardPlayerPing()) { // Let Geyser handle sending the keep alive downstream.setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); } @@ -1029,7 +1021,7 @@ public void packetSending(PacketSendingEvent event) { ClientIntentionPacket intentionPacket = event.getPacket(); String address; - if (geyser.getConfig().getRemote().isForwardHost()) { + if (geyser.config().java().forwardHostname()) { address = clientData.getServerAddress().split(":")[0]; } else { address = intentionPacket.getHostname(); @@ -1096,7 +1088,7 @@ public void disconnected(DisconnectedEvent event) { disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.authentication_type_mismatch", locale()); // Explain that they may be looking for Floodgate. geyser.getLogger().warning(GeyserLocale.getLocaleStringLog( - geyser.getPlatformType() == PlatformType.STANDALONE ? + geyser.platformType() == PlatformType.STANDALONE ? "geyser.network.remote.floodgate_explanation_standalone" : "geyser.network.remote.floodgate_explanation_plugin", Constants.FLOODGATE_DOWNLOAD_LOCATION @@ -1104,7 +1096,7 @@ public void disconnected(DisconnectedEvent event) { } else { // Likely that Floodgate is not configured correctly. disconnectMessage = GeyserLocale.getPlayerLocaleString("geyser.network.remote.floodgate_login_error", locale()); - if (geyser.getPlatformType() == PlatformType.STANDALONE) { + if (geyser.platformType() == PlatformType.STANDALONE) { geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.remote.floodgate_login_error_standalone")); } } @@ -1126,7 +1118,7 @@ public void disconnected(DisconnectedEvent event) { } else { GeyserImpl.getInstance().getLogger().error("An exception occurred: ", cause); } - if (geyser.getConfig().isDebugMode()) { + if (geyser.config().debugMode()) { cause.printStackTrace(); } } @@ -1149,7 +1141,7 @@ public void packetReceived(Session session, Packet packet) { @Override public void packetError(PacketErrorEvent event) { geyser.getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); - if (geyser.getConfig().isDebugMode()) + if (geyser.config().debugMode()) event.getCause().printStackTrace(); event.setSuppress(true); } @@ -1181,7 +1173,7 @@ public void disconnect(String reason) { } else { // Downstream's disconnect will fire an event that prints a log message // Otherwise, we print a message here - String address = geyser.getConfig().isLogPlayerIpAddresses() ? upstream.getAddress().getAddress().toString() : ""; + String address = geyser.config().logPlayerIpAddresses() ? upstream.getAddress().getAddress().toString() : ""; geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.disconnect", address, reason)); } @@ -1615,7 +1607,7 @@ private void startGame() { startGamePacket.setLevelGameType(GameType.SURVIVAL); startGamePacket.setDifficulty(1); startGamePacket.setDefaultSpawn(Vector3i.ZERO); - startGamePacket.setAchievementsDisabled(!geyser.getConfig().isXboxAchievementsEnabled()); + startGamePacket.setAchievementsDisabled(!geyser.config().xboxAchievementsEnabled()); startGamePacket.setCurrentTick(-1); startGamePacket.setEduEditionOffers(0); startGamePacket.setEduFeaturesEnabled(false); @@ -1625,7 +1617,7 @@ private void startGame() { startGamePacket.setBroadcastingToLan(true); startGamePacket.setPlatformBroadcastMode(GamePublishSetting.PUBLIC); startGamePacket.setXblBroadcastMode(GamePublishSetting.PUBLIC); - startGamePacket.setCommandsEnabled(!geyser.getConfig().isXboxAchievementsEnabled()); + startGamePacket.setCommandsEnabled(!geyser.config().xboxAchievementsEnabled()); startGamePacket.setTexturePacksRequired(false); startGamePacket.setBonusChestEnabled(false); startGamePacket.setStartingWithMap(false); @@ -1643,7 +1635,7 @@ private void startGame() { startGamePacket.setEducationProductionId(""); startGamePacket.setForceExperimentalGameplay(OptionalBoolean.empty()); - String serverName = geyser.getConfig().getBedrock().serverName(); + String serverName = geyser.config().bedrock().serverName(); startGamePacket.setLevelId(serverName); startGamePacket.setLevelName(serverName); @@ -1767,7 +1759,7 @@ public void sendDownstreamLoginPacket(Packet packet) { public void sendDownstreamPacket(Packet packet, ProtocolState intendedState) { // protocol can be null when we're not yet logged in (online auth) if (protocol == null) { - if (geyser.getConfig().isDebugMode()) { + if (geyser.config().debugMode()) { geyser.getLogger().debug("Tried to send downstream packet with no downstream session!"); Thread.dumpStack(); } @@ -1793,7 +1785,7 @@ public void sendDownstreamPacket(Packet packet) { if (channel == null) { // Channel is only null before the connection has initialized geyser.getLogger().warning("Tried to send a packet to the Java server too early!"); - if (geyser.getConfig().isDebugMode()) { + if (geyser.config().debugMode()) { Thread.dumpStack(); } return; @@ -2256,7 +2248,7 @@ public void removeCommandEnum(String name, String enums) { private void softEnumPacket(String name, SoftEnumUpdateType type, String enums) { // There is no need to send command enums if command suggestions are disabled - if (!this.geyser.getConfig().isCommandSuggestions()) { + if (!this.geyser.config().commandSuggestions()) { return; } UpdateSoftEnumPacket packet = new UpdateSoftEnumPacket(); diff --git a/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java b/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java index 07dd3849185..8aad61ede2b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java +++ b/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java @@ -25,93 +25,97 @@ package org.geysermc.geyser.session.auth; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; import lombok.Getter; import lombok.Setter; import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.InputMode; import org.geysermc.floodgate.util.UiProfile; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; import java.util.UUID; -@JsonIgnoreProperties(ignoreUnknown = true) @Getter public final class BedrockClientData { - @JsonProperty(value = "GameVersion") + @SerializedName(value = "GameVersion") private String gameVersion; - @JsonProperty(value = "ServerAddress") + @SerializedName(value = "ServerAddress") private String serverAddress; - @JsonProperty(value = "ThirdPartyName") + @SerializedName(value = "ThirdPartyName") private String username; - @JsonProperty(value = "LanguageCode") + @SerializedName(value = "LanguageCode") private String languageCode; - @JsonProperty(value = "SkinId") + @SerializedName(value = "SkinId") private String skinId; - @JsonProperty(value = "SkinData") + @SerializedName(value = "SkinData") private String skinData; - @JsonProperty(value = "SkinImageHeight") + @SerializedName(value = "SkinImageHeight") private int skinImageHeight; - @JsonProperty(value = "SkinImageWidth") + @SerializedName(value = "SkinImageWidth") private int skinImageWidth; - @JsonProperty(value = "CapeId") + @SerializedName(value = "CapeId") private String capeId; - @JsonProperty(value = "CapeData") + @SerializedName(value = "CapeData") + @JsonAdapter(value = StringToByteDeserializer.class) private byte[] capeData; - @JsonProperty(value = "CapeImageHeight") + @SerializedName(value = "CapeImageHeight") private int capeImageHeight; - @JsonProperty(value = "CapeImageWidth") + @SerializedName(value = "CapeImageWidth") private int capeImageWidth; - @JsonProperty(value = "CapeOnClassicSkin") + @SerializedName(value = "CapeOnClassicSkin") private boolean capeOnClassicSkin; - @JsonProperty(value = "SkinResourcePatch") + @SerializedName(value = "SkinResourcePatch") private String geometryName; - @JsonProperty(value = "SkinGeometryData") + @SerializedName(value = "SkinGeometryData") private String geometryData; - @JsonProperty(value = "PersonaSkin") + @SerializedName(value = "PersonaSkin") private boolean personaSkin; - @JsonProperty(value = "PremiumSkin") + @SerializedName(value = "PremiumSkin") private boolean premiumSkin; - @JsonProperty(value = "DeviceId") + @SerializedName(value = "DeviceId") private String deviceId; - @JsonProperty(value = "DeviceModel") + @SerializedName(value = "DeviceModel") private String deviceModel; - @JsonProperty(value = "DeviceOS") + @SerializedName(value = "DeviceOS") private DeviceOs deviceOs; - @JsonProperty(value = "UIProfile") + @SerializedName(value = "UIProfile") private UiProfile uiProfile; - @JsonProperty(value = "GuiScale") + @SerializedName(value = "GuiScale") private int guiScale; - @JsonProperty(value = "CurrentInputMode") + @SerializedName(value = "CurrentInputMode") private InputMode currentInputMode; - @JsonProperty(value = "DefaultInputMode") + @SerializedName(value = "DefaultInputMode") private InputMode defaultInputMode; - @JsonProperty("PlatformOnlineId") + @SerializedName("PlatformOnlineId") private String platformOnlineId; - @JsonProperty(value = "PlatformOfflineId") + @SerializedName(value = "PlatformOfflineId") private String platformOfflineId; - @JsonProperty(value = "SelfSignedId") + @SerializedName(value = "SelfSignedId") private UUID selfSignedId; - @JsonProperty(value = "ClientRandomId") + @SerializedName(value = "ClientRandomId") private long clientRandomId; - @JsonProperty(value = "ArmSize") + @SerializedName(value = "ArmSize") private String armSize; - @JsonProperty(value = "SkinAnimationData") + @SerializedName(value = "SkinAnimationData") private String skinAnimationData; - @JsonProperty(value = "SkinColor") + @SerializedName(value = "SkinColor") private String skinColor; - @JsonProperty(value = "ThirdPartyNameOnly") + @SerializedName(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; - @JsonProperty(value = "PlayFabId") + @SerializedName(value = "PlayFabId") private String playFabId; - @JsonIgnore @Setter - private String originalString = null; + private transient String originalString = null; public DeviceOs getDeviceOs() { return deviceOs != null ? deviceOs : DeviceOs.UNKNOWN; @@ -128,4 +132,11 @@ public InputMode getDefaultInputMode() { public UiProfile getUiProfile() { return uiProfile != null ? uiProfile : UiProfile.CLASSIC; } + + private static final class StringToByteDeserializer implements JsonDeserializer { + @Override + public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return json.getAsString().getBytes(StandardCharsets.UTF_8); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java index 2aeb83fa87c..7eee215fa28 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/PreferencesCache.java @@ -27,7 +27,7 @@ import lombok.Getter; import lombok.Setter; -import org.geysermc.geyser.configuration.GeyserConfiguration; +import org.geysermc.geyser.configuration.GeyserConfig; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.CooldownUtils; @@ -54,15 +54,16 @@ public class PreferencesCache { private boolean prefersCustomSkulls; /** - * Which CooldownType the client prefers. Initially set to {@link CooldownUtils#getDefaultShowCooldown()}. + * Which CooldownType the client prefers. Initially set to the config default. */ @Setter - private CooldownUtils.CooldownType cooldownPreference = CooldownUtils.getDefaultShowCooldown(); + private CooldownUtils.CooldownType cooldownPreference; public PreferencesCache(GeyserSession session) { this.session = session; - prefersCustomSkulls = session.getGeyser().getConfig().isAllowCustomSkulls(); + prefersCustomSkulls = session.getGeyser().config().allowCustomSkulls(); + cooldownPreference = session.getGeyser().config().showCooldown(); } /** @@ -71,10 +72,10 @@ public PreferencesCache(GeyserSession session) { * If {@link #prefersShowCoordinates} is true, coordinates will be shown, unless either of the following conditions apply:
*
* {@link GeyserSession#isReducedDebugInfo()} is enabled - * {@link GeyserConfiguration#isShowCoordinates()} is disabled + * {@link GeyserConfig#showCoordinates()} is disabled */ public void updateShowCoordinates() { - allowShowCoordinates = !session.isReducedDebugInfo() && session.getGeyser().getConfig().isShowCoordinates(); + allowShowCoordinates = !session.isReducedDebugInfo() && session.getGeyser().config().showCoordinates(); session.sendGameRule("showcoordinates", allowShowCoordinates && prefersShowCoordinates); } @@ -82,6 +83,6 @@ public void updateShowCoordinates() { * @return true if the session prefers custom skulls, and the config allows them. */ public boolean showCustomSkulls() { - return prefersCustomSkulls && session.getGeyser().getConfig().isAllowCustomSkulls(); + return prefersCustomSkulls && session.getGeyser().config().allowCustomSkulls(); } } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java index 0eec39b0bee..46160545ed2 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java @@ -74,11 +74,11 @@ public class SkullCache { public SkullCache(GeyserSession session) { this.session = session; - this.maxVisibleSkulls = session.getGeyser().getConfig().getMaxVisibleCustomSkulls(); + this.maxVisibleSkulls = session.getGeyser().config().advanced().maxVisibleCustomSkulls(); this.cullingEnabled = this.maxVisibleSkulls != -1; // Normal skulls are not rendered beyond 64 blocks - int distance = Math.min(session.getGeyser().getConfig().getCustomSkullRenderDistance(), 64); + int distance = Math.min(session.getGeyser().config().advanced().customSkullRenderDistance(), 64); this.skullRenderDistanceSquared = distance * distance; } @@ -98,7 +98,7 @@ public Skull putSkull(Vector3i position, UUID uuid, String texturesProperty, Blo } } catch (IOException e) { session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty); - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { e.printStackTrace(); } } diff --git a/core/src/main/java/org/geysermc/geyser/skin/FloodgateSkinUploader.java b/core/src/main/java/org/geysermc/geyser/skin/FloodgateSkinUploader.java index 4e4f529147c..415b6fa4bea 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FloodgateSkinUploader.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FloodgateSkinUploader.java @@ -25,11 +25,9 @@ package org.geysermc.geyser.skin; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import lombok.Getter; import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.floodgate.util.WebsocketEventType; @@ -37,6 +35,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.JsonUtils; import org.geysermc.geyser.util.PluginMessageUtils; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; @@ -52,7 +51,6 @@ import java.util.concurrent.TimeUnit; public final class FloodgateSkinUploader { - private final ObjectMapper JACKSON = new ObjectMapper(); private final List skinQueue = new ArrayList<>(); private final GeyserLogger logger; @@ -79,15 +77,14 @@ public void onOpen(ServerHandshake handshake) { @Override public void onMessage(String message) { - // The reason why I don't like Jackson try { - JsonNode node = JACKSON.readTree(message); + JsonObject node = JsonUtils.parseJson(message); if (node.has("error")) { - logger.error("Got an error: " + node.get("error").asText()); + logger.error("Got an error: " + node.get("error").getAsString()); return; } - int typeId = node.get("event_id").asInt(); + int typeId = node.get("event_id").getAsInt(); WebsocketEventType type = WebsocketEventType.fromId(typeId); if (type == null) { logger.warning(String.format( @@ -98,11 +95,11 @@ public void onMessage(String message) { switch (type) { case SUBSCRIBER_CREATED: - id = node.get("id").asInt(); - verifyCode = node.get("verify_code").asText(); + id = node.get("id").getAsInt(); + verifyCode = node.get("verify_code").getAsString(); break; case SUBSCRIBER_COUNT: - subscribersCount = node.get("subscribers_count").asInt(); + subscribersCount = node.get("subscribers_count").getAsInt(); break; case SKIN_UPLOADED: // if Geyser is the only subscriber we have send it to the server manually @@ -111,19 +108,19 @@ public void onMessage(String message) { break; } - String xuid = node.get("xuid").asText(); + String xuid = node.get("xuid").getAsString(); GeyserSession session = geyser.connectionByXuid(xuid); if (session != null) { - if (!node.get("success").asBoolean()) { + if (!node.get("success").getAsBoolean()) { logger.info("Failed to upload skin for " + session.bedrockUsername()); return; } - JsonNode data = node.get("data"); + JsonObject data = node.getAsJsonObject("data"); - String value = data.get("value").asText(); - String signature = data.get("signature").asText(); + String value = data.get("value").getAsString(); + String signature = data.get("signature").getAsString(); byte[] bytes = (value + '\0' + signature) .getBytes(StandardCharsets.UTF_8); @@ -131,8 +128,8 @@ public void onMessage(String message) { } break; case LOG_MESSAGE: - String logMessage = node.get("message").asText(); - switch (node.get("priority").asInt()) { + String logMessage = node.get("message").getAsString(); + switch (node.get("priority").getAsInt()) { case -1 -> logger.debug("Got a message from skin uploader: " + logMessage); case 0 -> logger.info("Got a message from skin uploader: " + logMessage); case 1 -> logger.error("Got a message from skin uploader: " + logMessage); @@ -150,20 +147,19 @@ public void onMessage(String message) { @Override public void onClose(int code, String reason, boolean remote) { if (reason != null && !reason.isEmpty()) { - // The reason why I don't like Jackson try { - JsonNode node = JACKSON.readTree(reason); + JsonObject node = JsonUtils.parseJson(reason); // info means that the uploader itself did nothing wrong if (node.has("info")) { - String info = node.get("info").asText(); + String info = node.get("info").getAsString(); logger.debug("Got disconnected from the skin uploader: " + info); } // error means that the uploader did something wrong if (node.has("error")) { - String error = node.get("error").asText(); + String error = node.get("error").getAsString(); logger.info("Got disconnected from the skin uploader: " + error); } - } catch (JsonProcessingException ignored) { + } catch (JsonSyntaxException ignored) { // ignore invalid json } catch (Exception e) { logger.error("Error while handling onClose", e); @@ -195,20 +191,13 @@ public void uploadSkin(List chainData, String clientData) { return; } - ObjectNode node = JACKSON.createObjectNode(); - ArrayNode chainDataNode = JACKSON.createArrayNode(); + JsonObject node = new JsonObject(); + JsonArray chainDataNode = new JsonArray(); chainData.forEach(chainDataNode::add); - node.set("chain_data", chainDataNode); - node.put("client_data", clientData); - - // The reason why I don't like Jackson - String jsonString; - try { - jsonString = JACKSON.writeValueAsString(node); - } catch (Exception e) { - logger.error("Failed to upload skin", e); - return; - } + node.add("chain_data", chainDataNode); + node.addProperty("client_data", clientData); + + String jsonString = node.toString(); if (client.isOpen()) { client.send(jsonString); @@ -241,4 +230,4 @@ public void close() { client.close(); } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java index 4c3db7504c9..ea99597f1b1 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -25,7 +25,8 @@ package org.geysermc.geyser.skin; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; @@ -43,6 +44,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.auth.BedrockClientData; import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.JsonUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -194,7 +196,7 @@ public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSessio public static void handleBedrockSkin(PlayerEntity playerEntity, BedrockClientData clientData) { GeyserImpl geyser = GeyserImpl.getInstance(); - if (geyser.getConfig().isDebugMode()) { + if (geyser.config().debugMode()) { geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.skin.bedrock.register", playerEntity.getUsername(), playerEntity.getUuid())); } @@ -208,7 +210,7 @@ public static void handleBedrockSkin(PlayerEntity playerEntity, BedrockClientDat if (skinBytes.length <= (128 * 128 * 4) && !clientData.isPersonaSkin()) { SkinProvider.storeBedrockSkin(playerEntity.getUuid(), clientData.getSkinId(), skinBytes); SkinProvider.storeBedrockGeometry(playerEntity.getUuid(), geometryNameBytes, geometryBytes); - } else if (geyser.getConfig().isDebugMode()) { + } else if (geyser.config().debugMode()) { geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.skin.bedrock.fail", playerEntity.getUsername())); geyser.getLogger().debug("The size of '" + playerEntity.getUsername() + "' skin is: " + clientData.getSkinImageWidth() + "x" + clientData.getSkinImageHeight()); } @@ -246,7 +248,7 @@ public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) { return loadFromJson(skinDataValue); } catch (IOException e) { GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for tag " + tag); - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { e.printStackTrace(); } return null; @@ -274,7 +276,7 @@ public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) { } else { GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty); } - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { exception.printStackTrace(); } } @@ -282,29 +284,25 @@ public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) { } public static @Nullable GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { - JsonNode skinObject; + JsonObject skinObject; try { - skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8)); + skinObject = JsonUtils.parseJson(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8)); } catch (IllegalArgumentException e) { GeyserImpl.getInstance().getLogger().debug("Invalid base64 encoded skin entry: " + encodedJson); return null; } - JsonNode textures = skinObject.get("textures"); - - if (textures == null) { + if (!(skinObject.get("textures") instanceof JsonObject textures)) { return null; } - JsonNode skinTexture = textures.get("SKIN"); - if (skinTexture == null) { + if (!(textures.get("SKIN") instanceof JsonObject skinTexture)) { return null; } String skinUrl; - JsonNode skinUrlNode = skinTexture.get("url"); - if (skinUrlNode != null && skinUrlNode.isTextual()) { - skinUrl = skinUrlNode.asText().replace("http://", "https://"); + if (skinTexture.get("url") instanceof JsonPrimitive skinUrlNode && skinUrlNode.isString()) { + skinUrl = skinUrlNode.getAsString().replace("http://", "https://"); } else { return null; } @@ -321,11 +319,9 @@ public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) { boolean isAlex = skinTexture.has("metadata"); String capeUrl = null; - JsonNode capeTexture = textures.get("CAPE"); - if (capeTexture != null) { - JsonNode capeUrlNode = capeTexture.get("url"); - if (capeUrlNode != null && capeUrlNode.isTextual()) { - capeUrl = capeUrlNode.asText().replace("http://", "https://"); + if (textures.get("CAPE") instanceof JsonObject capeTexture) { + if (capeTexture.get("url") instanceof JsonPrimitive capeUrlNode && capeUrlNode.isString()) { + capeUrl = capeUrlNode.getAsString().replace("http://", "https://"); } } @@ -334,4 +330,4 @@ public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) { private static final String DEFAULT_FLOODGATE_STEVE = "https://textures.minecraft.net/texture/31f477eb1a7beee631c2ca64d06f8f68fa93a3386d04452ab27f43acdf1b60cb"; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index aec1fa4de4c..b7b9b732dd8 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -25,9 +25,11 @@ package org.geysermc.geyser.skin; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.bytes.ByteArrays; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -124,11 +126,6 @@ public class SkinProvider { WEARING_CUSTOM_SKULL = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkull\"}}", wearingCustomSkull); String wearingCustomSkullSlim = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json"), StandardCharsets.UTF_8); WEARING_CUSTOM_SKULL_SLIM = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkullSlim\"}}", wearingCustomSkullSlim); - - GeyserImpl geyser = GeyserImpl.getInstance(); - if (geyser.getConfig().isAllowThirdPartyEars() || geyser.getConfig().isAllowThirdPartyCapes()) { - geyser.getLogger().warning("Third-party ears/capes have been removed from Geyser, if you still wish to have this functionality please use the extension: https://github.com/GeyserMC/ThirdPartyCosmetics"); - } } public static ExecutorService getExecutorService() { @@ -147,7 +144,7 @@ public static void shutdown() { public static void registerCacheImageTask(GeyserImpl geyser) { // Schedule Daily Image Expiry if we are caching them - if (geyser.getConfig().getCacheImages() > 0) { + if (geyser.config().advanced().cacheImages() > 0) { geyser.getScheduledThread().scheduleAtFixedRate(() -> { File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").toFile(); if (!cacheFolder.exists()) { @@ -155,7 +152,7 @@ public static void registerCacheImageTask(GeyserImpl geyser) { } int count = 0; - final long expireTime = ((long) GeyserImpl.getInstance().getConfig().getCacheImages()) * ((long)1000 * 60 * 60 * 24); + final long expireTime = ((long) GeyserImpl.getInstance().config().advanced().cacheImages()) * ((long)1000 * 60 * 60 * 24); for (File imageFile : Objects.requireNonNull(cacheFolder.listFiles())) { if (imageFile.lastModified() < System.currentTimeMillis() - expireTime) { //noinspection ResultOfMethodCallIgnored @@ -186,7 +183,7 @@ static SkinData determineFallbackSkinData(UUID uuid) { Cape cape = null; SkinGeometry geometry = SkinGeometry.WIDE; - if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) { + if (GeyserImpl.getInstance().config().java().authType() != AuthType.ONLINE) { // Let's see if this player is a Bedrock player, and if so, let's pull their skin. GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid); if (session != null) { @@ -425,7 +422,7 @@ public static BufferedImage requestImage(String imageUrl, boolean isCape) throws GeyserImpl.getInstance().getLogger().debug("Downloaded " + imageUrl); // Write to cache if we are allowed - if (GeyserImpl.getInstance().getConfig().getCacheImages() > 0) { + if (GeyserImpl.getInstance().config().advanced().cacheImages() > 0) { imageFile.getParentFile().mkdirs(); try { ImageIO.write(image, "png", imageFile); @@ -485,16 +482,16 @@ private static byte[] requestImageData(String imageUrl, boolean isCape) throws E public static CompletableFuture<@Nullable String> requestTexturesFromUUID(String uuid) { return CompletableFuture.supplyAsync(() -> { try { - JsonNode node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid); - JsonNode properties = node.get("properties"); + JsonObject node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid); + JsonArray properties = node.getAsJsonArray("properties"); if (properties == null) { GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuid); return null; } - return node.get("properties").get(0).get("value").asText(); + return properties.get(0).getAsJsonObject().get("value").getAsString(); } catch (Exception e) { GeyserImpl.getInstance().getLogger().debug("Unable to request textures for " + uuid); - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { e.printStackTrace(); } return null; @@ -512,15 +509,15 @@ private static byte[] requestImageData(String imageUrl, boolean isCape) throws E return CompletableFuture.supplyAsync(() -> { try { // Offline skin, or no present UUID - JsonNode node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username); - JsonNode id = node.get("id"); + JsonObject node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username); + JsonElement id = node.get("id"); if (id == null) { GeyserImpl.getInstance().getLogger().debug("No UUID found in Mojang response for " + username); return null; } - return id.asText(); + return id.getAsString(); } catch (Exception e) { - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { e.printStackTrace(); } return null; diff --git a/core/src/main/java/org/geysermc/geyser/text/AsteriskSerializer.java b/core/src/main/java/org/geysermc/geyser/text/AsteriskSerializer.java index 66b61dbff0f..ec79fb9060c 100644 --- a/core/src/main/java/org/geysermc/geyser/text/AsteriskSerializer.java +++ b/core/src/main/java/org/geysermc/geyser/text/AsteriskSerializer.java @@ -25,84 +25,67 @@ package org.geysermc.geyser.text; -import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.BeanProperty; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.ContextualSerializer; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import org.spongepowered.configurate.objectmapping.meta.Processor; +import org.spongepowered.configurate.serialize.SerializationException; -import java.io.IOException; -import java.io.Serial; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.Optional; - -public class AsteriskSerializer extends StdSerializer implements ContextualSerializer { - - @Serial - private static final long serialVersionUID = 1L; +import java.lang.reflect.Type; +import java.net.InetAddress; +public class AsteriskSerializer implements JsonSerializer { public static final String[] NON_SENSITIVE_ADDRESSES = {"", "0.0.0.0", "localhost", "127.0.0.1", "auto", "unknown"}; public static boolean showSensitive = false; - @Target({ElementType.FIELD}) - @Retention(RetentionPolicy.RUNTIME) - @JacksonAnnotationsInside - @JsonSerialize(using = AsteriskSerializer.class) - public @interface Asterisk { - String value() default "***"; - /** - * If true, this value will be shown if {@link #showSensitive} is true, or if the IP is determined to not be a public IP - * - * @return true if this should be analyzed and treated as an IP - */ - boolean isIp() default false; - } - - String asterisk; - boolean isIp; - - @SuppressWarnings("unused") // Used by Jackson for Geyser dumps - public AsteriskSerializer() { - super(Object.class); - } - - public AsteriskSerializer(String asterisk, boolean isIp) { - super(Object.class); - this.asterisk = asterisk; - this.isIp = isIp; - } - - @Override - public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty property) { - Optional anno = Optional.ofNullable(property) - .map(prop -> prop.getAnnotation(Asterisk.class)); - - return new AsteriskSerializer(anno.map(Asterisk::value).orElse(null), anno.map(Asterisk::isIp).orElse(false)); - } - @Override - public void serialize(Object obj, JsonGenerator gen, SerializerProvider prov) throws IOException { - if (isIp && (showSensitive || !isSensitiveIp((String) obj))) { - gen.writeObject(obj); - return; + public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) { + if (showSensitive || !isSensitiveIp(src)) { + return new JsonPrimitive(src); } - gen.writeString(asterisk); + return new JsonPrimitive("***"); } - private boolean isSensitiveIp(String ip) { + private static boolean isSensitiveIp(String ip) { for (String address : NON_SENSITIVE_ADDRESSES) { if (address.equalsIgnoreCase(ip)) { return false; } } + + try { + InetAddress address = InetAddress.getByName(ip); + if (address.isSiteLocalAddress() || address.isLoopbackAddress()) { + return false; + } + } catch (Exception e) { + // Ignore + } + return true; } + + public static Processor.Factory CONFIGURATE_SERIALIZER = (data, fieldType) -> (value, destination) -> { + if (showSensitive || !isSensitiveIp(value)) { + return; + } + try { + destination.set("***"); + } catch (SerializationException e) { + throw new RuntimeException("Unable to censor IP address", e); // Error over silently printing an IP address. + } + }; + + @Target({ElementType.FIELD, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface Asterisk { + + } } diff --git a/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java b/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java index b8867c35641..acef5cc987f 100644 --- a/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java @@ -41,6 +41,7 @@ import java.util.Properties; public class GeyserLocale { + public static final String SYSTEM_LOCALE = "system"; /** * If we determine the default locale that the user wishes to use, use that locale @@ -80,8 +81,8 @@ public static void init(GeyserBootstrap bootstrap) { * Finalize the default locale, now that we know what the default locale should be. */ public static void finalizeDefaultLocale(GeyserImpl geyser) { - String newDefaultLocale = geyser.getConfig().getDefaultLocale(); - if (newDefaultLocale == null) { + String newDefaultLocale = geyser.config().defaultLocale(); + if (SYSTEM_LOCALE.equals(newDefaultLocale)) { // We want to use the system locale which is already loaded return; } diff --git a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java index 94d8b254fc7..3f3e8ff6fb1 100644 --- a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java @@ -25,11 +25,13 @@ package org.geysermc.geyser.text; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.util.AssetUtils; import org.geysermc.geyser.util.FileUtils; +import org.geysermc.geyser.util.JsonUtils; import org.geysermc.geyser.util.WebUtils; import java.io.FileNotFoundException; @@ -39,7 +41,6 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.HashMap; -import java.util.Iterator; import java.util.Locale; import java.util.Map; @@ -189,14 +190,12 @@ public static Map parseLangFile(Path localeFile, String locale) // Read the localefile try (InputStream localeStream = Files.newInputStream(localeFile, StandardOpenOption.READ)) { // Parse the file as json - JsonNode localeObj = GeyserImpl.JSON_MAPPER.readTree(localeStream); + JsonObject localeObj = JsonUtils.fromJson(localeStream); // Parse all the locale fields - Iterator> localeIterator = localeObj.fields(); Map langMap = new HashMap<>(); - while (localeIterator.hasNext()) { - Map.Entry entry = localeIterator.next(); - langMap.put(entry.getKey(), entry.getValue().asText()); + for (Map.Entry entry : localeObj.entrySet()) { + langMap.put(entry.getKey(), entry.getValue().getAsString()); } return langMap; } catch (FileNotFoundException e){ @@ -266,4 +265,4 @@ private static String byteArrayToHexString(byte[] b) { } return result.toString(); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index 15c19c542ad..4dd9a50f8e5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -235,7 +235,7 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven case PLACE: { TransferItemStackRequestAction transferAction = (TransferItemStackRequestAction) action; if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) { - if (session.getGeyser().getConfig().isDebugMode()) { + if (session.getGeyser().config().debugMode()) { session.getGeyser().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.bedrockUsername()); dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination()); } @@ -399,7 +399,7 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven ItemStackRequestSlotData destination = swapAction.getDestination(); if (!(checkNetId(session, inventory, source) && checkNetId(session, inventory, destination))) { - if (session.getGeyser().getConfig().isDebugMode()) { + if (session.getGeyser().config().debugMode()) { session.getGeyser().getLogger().error("DEBUG: About to reject SWAP request made by " + session.bedrockUsername()); dumpStackRequestDetails(session, inventory, source, destination); } @@ -899,7 +899,7 @@ protected static ItemStackResponse rejectRequest(ItemStackRequest request) { * as bad (false). */ protected static ItemStackResponse rejectRequest(ItemStackRequest request, boolean throwError) { - if (throwError && GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (throwError && GeyserImpl.getInstance().config().debugMode()) { new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace(); } return new ItemStackResponse(ItemStackResponseStatus.ERROR, request.getRequestId(), Collections.emptyList()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java index 77f58f5d634..61d58b722f9 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java @@ -123,7 +123,7 @@ private static UUID getUUID(NbtMap profile) { return skull.getBlockDefinition(); } catch (InterruptedException | ExecutionException e) { session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + javaNbt); - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (GeyserImpl.getInstance().config().debugMode()) { e.printStackTrace(); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java index 1e84f032e0b..1257e410f41 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java @@ -44,8 +44,8 @@ public void translate(GeyserSession session, CommandRequestPacket packet) { } static void handleCommand(GeyserSession session, String command) { - if (session.getGeyser().getPlatformType() == PlatformType.STANDALONE || - session.getGeyser().getPlatformType() == PlatformType.VIAPROXY) { + if (session.getGeyser().platformType() == PlatformType.STANDALONE || + session.getGeyser().platformType() == PlatformType.VIAPROXY) { // try to handle the command within the standalone/viaproxy command manager String[] args = command.split(" "); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockEmoteListTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockEmoteListTranslator.java index b20f7a2dd5b..a7d1c3c14bc 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockEmoteListTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockEmoteListTranslator.java @@ -26,7 +26,6 @@ package org.geysermc.geyser.translator.protocol.bedrock; import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket; -import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -36,10 +35,6 @@ public class BedrockEmoteListTranslator extends PacketTranslator yaw <= -135f || yaw > 135f; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java index 6ca0f35000b..bf60d4bb5cc 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java @@ -47,7 +47,7 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator= 0) { - if (session.getGeyser().getConfig().isForwardPlayerPing()) { + if (session.getGeyser().config().forwardPlayerPing()) { // use our cached value because // a) bedrock can be inaccurate with the value returned // b) playstation replies with a different magnitude than other platforms diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java index 47c5bfd35ac..fe07d8192c2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java @@ -45,7 +45,7 @@ public void translate(GeyserSession session, SetLocalPlayerAsInitializedPacket p if (session.remoteServer().authType() == AuthType.ONLINE) { if (!session.isLoggedIn()) { - if (session.getGeyser().getConfig().getSavedUserLogins().contains(session.bedrockUsername())) { + if (session.getGeyser().config().savedUserLogins().contains(session.bedrockUsername())) { if (session.getGeyser().authChainFor(session.bedrockUsername()) == null) { LoginEncryptionUtils.buildAndShowConsentWindow(session); } else { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java index 7a37aa72e4a..13683b2dc39 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java @@ -27,7 +27,6 @@ import org.cloudburstmc.protocol.bedrock.packet.EmotePacket; import org.geysermc.geyser.api.event.bedrock.ClientEmoteEvent; -import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.session.GeyserSession; @@ -39,15 +38,6 @@ public class BedrockEmoteTranslator extends PacketTranslator { @Override public void translate(GeyserSession session, EmotePacket packet) { - if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) { - // Activate the workaround - we should trigger the offhand now - session.requestOffhandSwap(); - - if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) { - return; - } - } - // For the future: could have a method that exposes which players will see the emote ClientEmoteEvent event = new ClientEmoteEvent(session, packet.getEmoteId()); session.getGeyser().eventBus().fire(event); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java index 5500deb8017..cd3262fd0a6 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java @@ -123,7 +123,7 @@ public boolean equals(BedrockCommandInfo a, BedrockCommandInfo b) { @Override public void translate(GeyserSession session, ClientboundCommandsPacket packet) { // Don't send command suggestions if they are disabled - if (!session.getGeyser().getConfig().isCommandSuggestions()) { + if (!session.getGeyser().config().commandSuggestions()) { session.getGeyser().getLogger().debug("Not sending translated command suggestions as they are disabled."); // Send a mostly empty packet so Bedrock doesn't override /help with its own, built-in help command. diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java index 399c2308085..e711c72c63f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java @@ -39,7 +39,7 @@ public class JavaKeepAliveTranslator extends PacketTranslator definition = entity.getDefinition(); for (EntityMetadata metadata : packet.getMetadata()) { if (metadata.getId() >= definition.translators().size()) { - if (session.getGeyser().getConfig().isDebugMode()) { + if (session.getGeyser().config().debugMode()) { // Minecraft client just ignores these session.getGeyser().getLogger().warning("Metadata ID " + metadata.getId() + " is out of bounds of known entity metadata size " + definition.translators().size() + " for entity type " + entity.getDefinition().entityType()); session.getGeyser().getLogger().debug(metadata.toString()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java index d56c5d2d36e..3ed044096ea 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java @@ -99,7 +99,7 @@ public void translate(GeyserSession session, ClientboundPlayerPositionPacket pac ChunkUtils.updateChunkPosition(session, pos.toInt()); - if (session.getGeyser().getConfig().isDebugMode()) { + if (session.getGeyser().config().debugMode()) { session.getGeyser().getLogger().debug("Spawned player at " + packet.getPosition()); } return; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java index c94468c17bd..8c953f8cfe5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java @@ -92,7 +92,7 @@ public void translate(GeyserSession session, ClientboundBlockEventPacket packet) // Retracting sticky pistons is an exception, since the event is not called on Spigot from 1.13.2 - 1.17.1 // See https://github.com/PaperMC/Paper/blob/6fa1983e9ce177a4a412d5b950fd978620174777/patches/server/0304-Fire-BlockPistonRetractEvent-for-all-empty-pistons.patch boolean isSticky = isSticky(pistonBlock); - if (session.getGeyser().getPlatformType() == PlatformType.SPIGOT && !isSticky) { + if (session.getGeyser().platformType() == PlatformType.SPIGOT && !isSticky) { return; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java index 5c3b21b5a8f..0ae7b742aaa 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java @@ -367,7 +367,7 @@ public static void handleChatPacket(GeyserSession session, Component message, Ho textPacket.setMessage(MessageTranslator.convertMessage(withDecoration.build(), session.locale())); } else { session.getGeyser().getLogger().debug("Likely illegal chat type detection found."); - if (session.getGeyser().getConfig().isDebugMode()) { + if (session.getGeyser().config().debugMode()) { Thread.dumpStack(); } textPacket.setMessage(MessageTranslator.convertMessage(message, session.locale())); diff --git a/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java b/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java index 6bdae6dfeda..81df9919db5 100644 --- a/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/AssetUtils.java @@ -25,18 +25,28 @@ package org.geysermc.geyser.util; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.annotations.SerializedName; import lombok.Getter; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.text.GeyserLocale; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.zip.ZipFile; @@ -83,7 +93,7 @@ public static CompletableFuture generateAssetCache() { return CompletableFuture.supplyAsync(() -> { try { // Get the version manifest from Mojang - VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue( + VersionManifest versionManifest = GeyserImpl.GSON.fromJson( WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class); // Get the url for the latest version of the games manifest @@ -101,26 +111,24 @@ public static CompletableFuture generateAssetCache() { } // Get the individual version manifest - VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class); + VersionInfo versionInfo = GeyserImpl.GSON.fromJson(WebUtils.getBody(latestInfoURL), VersionInfo.class); // Get the client jar for use when downloading the en_us locale - GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads())); + GeyserImpl.getInstance().getLogger().debug(versionInfo.getDownloads()); // Was previously a Jackson call for writeValueToString CLIENT_JAR_INFO = versionInfo.getDownloads().get("client"); - GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(CLIENT_JAR_INFO)); + GeyserImpl.getInstance().getLogger().debug(CLIENT_JAR_INFO); // Was previously a Jackson call for writeValueToString // Get the assets list - JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects"); + JsonObject assets = ((JsonObject) new JsonParser().parse(WebUtils.getBody(versionInfo.getAssetIndex().getUrl()))).getAsJsonObject("objects"); // Put each asset into an array for use later - Iterator> assetIterator = assets.fields(); - while (assetIterator.hasNext()) { - Map.Entry entry = assetIterator.next(); + for (Map.Entry entry : assets.entrySet()) { if (!entry.getKey().startsWith("minecraft/lang/")) { // No need to cache non-language assets as we don't use them continue; } - Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class); + Asset asset = GeyserImpl.GSON.fromJson(entry.getValue(), Asset.class); ASSET_MAP.put(entry.getKey(), asset); } @@ -221,106 +229,108 @@ public interface InputStreamConsumer { /* Classes that map to JSON files served by Mojang */ - @JsonIgnoreProperties(ignoreUnknown = true) @Getter static class VersionManifest { - @JsonProperty("latest") + @SerializedName("latest") private LatestVersion latestVersion; - @JsonProperty("versions") + @SerializedName("versions") private List versions; } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter static class LatestVersion { - @JsonProperty("release") + @SerializedName("release") private String release; - @JsonProperty("snapshot") + @SerializedName("snapshot") private String snapshot; } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter static class Version { - @JsonProperty("id") + @SerializedName("id") private String id; - @JsonProperty("type") + @SerializedName("type") private String type; - @JsonProperty("url") + @SerializedName("url") private String url; - @JsonProperty("time") + @SerializedName("time") private String time; - @JsonProperty("releaseTime") + @SerializedName("releaseTime") private String releaseTime; } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter static class VersionInfo { - @JsonProperty("id") + @SerializedName("id") private String id; - @JsonProperty("type") + @SerializedName("type") private String type; - @JsonProperty("time") + @SerializedName("time") private String time; - @JsonProperty("releaseTime") + @SerializedName("releaseTime") private String releaseTime; - @JsonProperty("assetIndex") + @SerializedName("assetIndex") private AssetIndex assetIndex; - @JsonProperty("downloads") + @SerializedName("downloads") private Map downloads; } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter static class VersionDownload { - @JsonProperty("sha1") + @SerializedName("sha1") private String sha1; - @JsonProperty("size") + @SerializedName("size") private int size; - @JsonProperty("url") + @SerializedName("url") private String url; + + @Override + public String toString() { + return "VersionDownload{" + + "sha1='" + sha1 + '\'' + + ", size=" + size + + ", url='" + url + '\'' + + '}'; + } } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter static class AssetIndex { - @JsonProperty("id") + @SerializedName("id") private String id; - @JsonProperty("sha1") + @SerializedName("sha1") private String sha1; - @JsonProperty("size") + @SerializedName("size") private int size; - @JsonProperty("totalSize") + @SerializedName("totalSize") private int totalSize; - @JsonProperty("url") + @SerializedName("url") private String url; } - @JsonIgnoreProperties(ignoreUnknown = true) @Getter public static class Asset { - @JsonProperty("hash") + @SerializedName("hash") private String hash; - @JsonProperty("size") + @SerializedName("size") private int size; } diff --git a/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java b/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java index c020e96b2c6..50faf8ae238 100644 --- a/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/CooldownUtils.java @@ -38,22 +38,12 @@ * Much of the work here is from the wonderful folks from ViaRewind */ public class CooldownUtils { - private static CooldownType DEFAULT_SHOW_COOLDOWN; - - public static void setDefaultShowCooldown(String showCooldown) { - DEFAULT_SHOW_COOLDOWN = CooldownType.getByName(showCooldown); - } - - public static CooldownType getDefaultShowCooldown() { - return DEFAULT_SHOW_COOLDOWN; - } - /** * Starts sending the fake cooldown to the Bedrock client. If the cooldown is not disabled, the sent type is the cooldownPreference in {@link PreferencesCache} * @param session GeyserSession */ public static void sendCooldown(GeyserSession session) { - if (DEFAULT_SHOW_COOLDOWN == CooldownType.DISABLED) return; + if (session.getGeyser().config().showCooldown() == CooldownType.DISABLED) return; CooldownType sessionPreference = session.getPreferencesCache().getCooldownPreference(); if (sessionPreference == CooldownType.DISABLED) return; @@ -161,10 +151,6 @@ public enum CooldownType { * @return The converted CooldownType */ public static CooldownType getByName(String name) { - if (name.equalsIgnoreCase("true")) { // Backwards config compatibility - return CooldownType.TITLE; - } - for (CooldownType type : VALUES) { if (type.name().equalsIgnoreCase(name)) { return type; diff --git a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java index 87ed8af02c3..dc208a80212 100644 --- a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java @@ -25,15 +25,17 @@ package org.geysermc.geyser.util; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; - -import java.io.*; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.annotation.Annotation; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -44,28 +46,18 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -public class FileUtils { - - /** - * Load the given YAML file into the given class - * - * @param src File to load - * @param valueType Class to load file into - * @param the type - * @return The data as the given class - * @throws IOException if the config could not be loaded - */ +public final class FileUtils { public static T loadConfig(File src, Class valueType) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()) - // Allow inference of single values as arrays - .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); - return objectMapper.readValue(src, valueType); + YamlConfigurationLoader loader = YamlConfigurationLoader.builder() + .file(src) + .build(); + ConfigurationNode node = loader.load(); + return node.get(valueType); } - public static T loadJson(InputStream src, Class valueType) throws IOException { + public static T loadJson(InputStream src, Class valueType) { // Read specifically with UTF-8 to allow any non-UTF-encoded JSON to read - return GeyserImpl.JSON_MAPPER.readValue(new InputStreamReader(src, StandardCharsets.UTF_8), valueType); + return GeyserImpl.GSON.fromJson(new InputStreamReader(src, StandardCharsets.UTF_8), valueType); } /** @@ -254,4 +246,7 @@ public static Set> getGeneratedClassesForAnnotation(String input) { throw new RuntimeException(e); } } + + private FileUtils() { + } } diff --git a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java index 9862eb730fe..045b439843d 100644 --- a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java @@ -196,7 +196,7 @@ public static void updateCursor(GeyserSession session) { } public static boolean canStack(GeyserItemStack item1, GeyserItemStack item2) { - if (GeyserImpl.getInstance().getConfig().isDebugMode()) + if (GeyserImpl.getInstance().config().debugMode()) canStackDebug(item1, item2); if (item1.isEmpty() || item2.isEmpty()) return false; @@ -248,7 +248,7 @@ public static IntFunction createUnusableSpaceBlock(String description) private static ItemDefinition getUnusableSpaceBlockDefinition(int protocolVersion) { ItemMappings mappings = Registries.ITEMS.forVersion(protocolVersion); - String unusableSpaceBlock = GeyserImpl.getInstance().getConfig().getUnusableSpaceBlock(); + String unusableSpaceBlock = GeyserImpl.getInstance().config().advanced().unusableSpaceBlock(); ItemDefinition itemDefinition = mappings.getDefinition(unusableSpaceBlock); if (itemDefinition == null) { diff --git a/core/src/main/java/org/geysermc/geyser/util/JsonUtils.java b/core/src/main/java/org/geysermc/geyser/util/JsonUtils.java new file mode 100644 index 00000000000..d1e4a9d99cc --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/util/JsonUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import marcono1234.gson.recordadapter.RecordTypeAdapterFactory; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.pack.GeyserResourcePackManifest; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; + +public final class JsonUtils { + + public static T fromJson(byte[] bytes, Class type) { + return GeyserImpl.GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), type); + } + + public static JsonObject fromJson(InputStream stream) { + return (JsonObject) new JsonParser().parse(new InputStreamReader(stream)); + } + + public static JsonObject parseJson(String s) { + return (JsonObject) new JsonParser().parse(s); + } + + public static T fromJson(InputStream stream, Type type) { + return GeyserImpl.GSON.fromJson(new InputStreamReader(stream), type); + } + + public static Gson createGson() { + GsonBuilder builder = new GsonBuilder().setPrettyPrinting(); + try { + new Gson().fromJson("{\"version\":[1,0,0],\"uuid\":\"eebb4ea8-a701-11eb-95ba-047d7bb283ba\"}", GeyserResourcePackManifest.Dependency.class); + } catch (Throwable e) { + // 1.16.5 and 1.17.1 (at minimum) have an outdated Gson version that doesn't support records. + // Remove this workaround when all platforms support Gson 2.10+ + // (Explicitly allow missing component values - the dependencies module for resource packs, for example, can be missing) + builder.registerTypeAdapterFactory(RecordTypeAdapterFactory.builder().allowMissingComponentValues().create()) + // Since this is a record, the above will take precedence unless we explicitly declare it. + .registerTypeAdapter(GeyserResourcePackManifest.Version.class, new GeyserResourcePackManifest.Version.VersionDeserializer()); + } + return builder.create(); + } + + private JsonUtils() { + } +} diff --git a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java index d3024be65f1..0eeae659fdf 100644 --- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java @@ -25,9 +25,6 @@ package org.geysermc.geyser.util; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode; import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket; @@ -53,8 +50,6 @@ import java.util.function.BiConsumer; public class LoginEncryptionUtils { - private static final ObjectMapper JSON_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - private static boolean HAS_SENT_ENCRYPTION_MESSAGE = false; public static void encryptPlayerConnection(GeyserSession session, LoginPacket loginPacket) { @@ -69,7 +64,7 @@ private static void encryptConnectionWithCert(GeyserSession session, String clie geyser.getLogger().debug(String.format("Is player data signed? %s", result.signed())); - if (!result.signed() && !session.getGeyser().getConfig().isEnableProxyConnections()) { + if (!result.signed() && !session.getGeyser().config().enableProxyConnections()) { session.disconnect(GeyserLocale.getLocaleStringLog("geyser.network.remote.invalid_xbox_account")); return; } @@ -85,8 +80,7 @@ private static void encryptConnectionWithCert(GeyserSession session, String clie throw new IllegalStateException("Client data isn't signed by the given chain data"); } - JsonNode clientDataJson = JSON_MAPPER.readTree(clientDataPayload); - BedrockClientData data = JSON_MAPPER.convertValue(clientDataJson, BedrockClientData.class); + BedrockClientData data = JsonUtils.fromJson(clientDataPayload, BedrockClientData.class); data.setOriginalString(clientData); session.setClientData(data); @@ -94,7 +88,7 @@ private static void encryptConnectionWithCert(GeyserSession session, String clie startEncryptionHandshake(session, identityPublicKey); } catch (Throwable e) { // An error can be thrown on older Java 8 versions about an invalid key - if (geyser.getConfig().isDebugMode()) { + if (geyser.config().debugMode()) { e.printStackTrace(); } @@ -213,7 +207,7 @@ public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, StepMs .append("\n%xbox.signin.enterCode\n") .append(ChatColor.GREEN) .append(msCode.getUserCode()); - int timeout = session.getGeyser().getConfig().getPendingAuthenticationTimeout(); + int timeout = session.getGeyser().config().pendingAuthenticationTimeout(); if (timeout != 0) { message.append("\n\n") .append(ChatColor.RESET) diff --git a/core/src/main/java/org/geysermc/geyser/util/Metrics.java b/core/src/main/java/org/geysermc/geyser/util/Metrics.java deleted file mode 100644 index a4ef301e393..00000000000 --- a/core/src/main/java/org/geysermc/geyser/util/Metrics.java +++ /dev/null @@ -1,447 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.util; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.GeyserImpl; - -import javax.net.ssl.HttpsURLConnection; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.zip.GZIPOutputStream; - -/** - * bStats collects some data for plugin authors. - *

- * Check out bStats to learn more about bStats! - */ -public class Metrics { - - // The version of this bStats class - public static final int B_STATS_VERSION = 1; - - // The url to which the data is sent - private static final String URL = "https://bStats.org/submitData/server-implementation"; - - // Should failed requests be logged? - private static boolean logFailedRequests = false; - - // The logger for the failed requests - private static Logger logger = Logger.getLogger("bStats"); - - // The name of the server software - private final String name; - - // The uuid of the server - private final String serverUUID; - - // A list with all custom charts - private final List charts = new ArrayList<>(); - - private final static ObjectMapper mapper = new ObjectMapper(); - - private final GeyserImpl geyser; - - /** - * Class constructor. - * - * @param geyser The Geyser instance - * @param name The name of the server software. - * @param serverUUID The uuid of the server. - * @param logFailedRequests Whether failed requests should be logged or not. - * @param logger The logger for the failed requests. - */ - public Metrics(GeyserImpl geyser, String name, String serverUUID, boolean logFailedRequests, Logger logger) { - this.geyser = geyser; - this.name = name; - this.serverUUID = serverUUID; - Metrics.logFailedRequests = logFailedRequests; - Metrics.logger = logger; - - // Start submitting the data - startSubmitting(); - } - - /** - * Adds a custom chart. - * - * @param chart The chart to add. - */ - public void addCustomChart(CustomChart chart) { - if (chart == null) { - throw new IllegalArgumentException("Chart cannot be null!"); - } - charts.add(chart); - } - - /** - * Starts the Scheduler which submits our data every 30 minutes. - */ - private void startSubmitting() { - geyser.getScheduledThread().scheduleAtFixedRate(this::submitData, 1, 30, TimeUnit.MINUTES); - // Submit the data every 30 minutes, first time after 1 minutes to give other plugins enough time to start - // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! - // WARNING: Just don't do it! - } - - /** - * Gets the plugin specific data. - * - * @return The plugin specific data. - */ - private ObjectNode getPluginData() { - ObjectNode data = mapper.createObjectNode(); - - data.put("pluginName", name); // Append the name of the server software - data.put("pluginVersion", GeyserImpl.VERSION); // Append the name of the server software - - ArrayNode customCharts = mapper.createArrayNode(); - for (CustomChart customChart : charts) { - // Add the data of the custom charts - JsonNode chart = customChart.getRequestJsonNode(); - if (chart == null) { // If the chart is null, we skip it - continue; - } - customCharts.add(chart); - } - data.set("customCharts", customCharts); - - return data; - } - - /** - * Gets the server specific data. - * - * @return The server specific data. - */ - private ObjectNode getServerData() { - // OS specific data - String osName = System.getProperty("os.name"); - String osArch = System.getProperty("os.arch"); - String osVersion = System.getProperty("os.version"); - int coreCount = Runtime.getRuntime().availableProcessors(); - - ObjectNode data = mapper.createObjectNode(); - - data.put("serverUUID", serverUUID); - - data.put("osName", osName); - data.put("osArch", osArch); - data.put("osVersion", osVersion); - data.put("coreCount", coreCount); - - return data; - } - - /** - * Collects the data and sends it afterwards. - */ - private void submitData() { - final ObjectNode data = getServerData(); - - ArrayNode pluginData = mapper.createArrayNode(); - pluginData.add(getPluginData()); - data.putPOJO("plugins", pluginData); - - new Thread(() -> { - try { - // We are still in the Thread of the timer, so nothing get blocked :) - sendData(data); - } catch (Exception e) { - // Something went wrong! :( - if (logFailedRequests) { - logger.log(Level.WARNING, "Could not submit stats of " + name, e); - } - } - }).start(); - } - - /** - * Sends the data to the bStats server. - * - * @param data The data to send. - * @throws Exception If the request failed. - */ - private static void sendData(ObjectNode data) throws Exception { - if (data == null) { - throw new IllegalArgumentException("Data cannot be null!"); - } - HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); - - // Compress the data to save bandwidth - byte[] compressedData = compress(data.toString()); - - // Add headers - connection.setRequestMethod("POST"); - connection.addRequestProperty("Accept", "application/json"); - connection.addRequestProperty("Connection", "close"); - connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request - connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); - connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format - connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); - - // Send data - connection.setDoOutput(true); - DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); - outputStream.write(compressedData); - outputStream.flush(); - outputStream.close(); - - connection.getInputStream().close(); // We don't care about the response - Just send our data :) - } - - /** - * Gzips the given String. - * - * @param str The string to gzip. - * @return The gzipped String. - * @throws IOException If the compression failed. - */ - private static byte @NonNull [] compress(final @NonNull String str) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - GZIPOutputStream gzip = new GZIPOutputStream(outputStream); - gzip.write(str.getBytes(StandardCharsets.UTF_8)); - gzip.close(); - return outputStream.toByteArray(); - } - - /** - * Represents a custom chart. - */ - public static abstract class CustomChart { - - // The id of the chart - final String chartId; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - */ - CustomChart(String chartId) { - if (chartId == null || chartId.isEmpty()) { - throw new IllegalArgumentException("ChartId cannot be null or empty!"); - } - this.chartId = chartId; - } - - private @Nullable ObjectNode getRequestJsonNode() { - ObjectNode chart = new ObjectMapper().createObjectNode(); - chart.put("chartId", chartId); - try { - ObjectNode data = getChartData(); - if (data == null) { - // If the data is null we don't send the chart. - return null; - } - chart.putPOJO("data", data); - } catch (Throwable t) { - if (logFailedRequests) { - logger.log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t); - } - return null; - } - return chart; - } - - - - protected abstract ObjectNode getChartData() throws Exception; - - } - - /** - * Represents a custom simple pie. - */ - public static class SimplePie extends CustomChart { - - private final Callable callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SimplePie(String chartId, Callable callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected @Nullable ObjectNode getChartData() throws Exception { - ObjectNode data = mapper.createObjectNode(); - String value = callable.call(); - if (value == null || value.isEmpty()) { - // Null = skip the chart - return null; - } - data.put("value", value); - return data; - } - } - - /** - * Represents a custom advanced pie. - */ - public static class AdvancedPie extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public AdvancedPie(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected @Nullable ObjectNode getChartData() throws Exception { - ObjectNode data = mapper.createObjectNode(); - ObjectNode values = mapper.createObjectNode(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == 0) { - continue; // Skip this invalid - } - allSkipped = false; - values.put(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - data.putPOJO("values", values); - return data; - } - } - - /** - * Represents a custom drilldown pie. - */ - public static class DrilldownPie extends CustomChart { - - private final Callable>> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public DrilldownPie(String chartId, Callable>> callable) { - super(chartId); - this.callable = callable; - } - - @Override - public @Nullable ObjectNode getChartData() throws Exception { - ObjectNode data = mapper.createObjectNode(); - ObjectNode values = mapper.createObjectNode(); - Map> map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean reallyAllSkipped = true; - for (Map.Entry> entryValues : map.entrySet()) { - ObjectNode value = mapper.createObjectNode(); - boolean allSkipped = true; - for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { - value.put(valueEntry.getKey(), valueEntry.getValue()); - allSkipped = false; - } - if (!allSkipped) { - reallyAllSkipped = false; - values.putPOJO(entryValues.getKey(), value); - } - } - if (reallyAllSkipped) { - // Null = skip the chart - return null; - } - data.putPOJO("values", values); - return data; - } - } - - /** - * Represents a custom single line chart. - */ - public static class SingleLineChart extends CustomChart { - - private final Callable callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SingleLineChart(String chartId, Callable callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected @Nullable ObjectNode getChartData() throws Exception { - ObjectNode data = mapper.createObjectNode(); - int value = callable.call(); - if (value == 0) { - // Null = skip the chart - return null; - } - data.put("value", value); - return data; - } - - } - -} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java index cb6ad6f0c2f..91760756048 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java @@ -54,8 +54,8 @@ public static CustomForm buildForm(GeyserSession session) { // Let's store these to avoid issues boolean showCoordinates = session.getPreferencesCache().isAllowShowCoordinates(); - boolean cooldownShown = CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED; - boolean customSkulls = session.getGeyser().getConfig().isAllowCustomSkulls(); + boolean cooldownShown = session.getGeyser().config().showCooldown() != CooldownUtils.CooldownType.DISABLED; + boolean customSkulls = session.getGeyser().config().allowCustomSkulls(); // Only show the client title if any of the client settings are available boolean showClientSettings = showCoordinates || cooldownShown || customSkulls; diff --git a/core/src/main/java/org/geysermc/geyser/util/VersionCheckUtils.java b/core/src/main/java/org/geysermc/geyser/util/VersionCheckUtils.java index cf90a6bcdc0..91e7fcbdc01 100644 --- a/core/src/main/java/org/geysermc/geyser/util/VersionCheckUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/VersionCheckUtils.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.util; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.event.ClickEvent; @@ -92,9 +92,9 @@ public static void checkForOutdatedJava(GeyserLogger logger) { public static void checkForGeyserUpdate(Supplier recipient) { CompletableFuture.runAsync(() -> { try { - JsonNode json = WebUtils.getJson("https://api.geysermc.org/v2/versions/geyser"); - JsonNode bedrock = json.get("bedrock").get("protocol"); - int protocolVersion = bedrock.get("id").asInt(); + JsonObject json = WebUtils.getJson("https://api.geysermc.org/v2/versions/geyser"); + JsonObject bedrock = json.getAsJsonObject("bedrock").getAsJsonObject("protocol"); + int protocolVersion = bedrock.get("id").getAsInt(); if (GameProtocol.getBedrockCodec(protocolVersion) != null) { LATEST_BEDROCK_RELEASE = OptionalInt.empty(); // We support the latest version! No need to print a message. @@ -102,7 +102,7 @@ public static void checkForGeyserUpdate(Supplier recipient) } LATEST_BEDROCK_RELEASE = OptionalInt.of(protocolVersion); - final String newBedrockVersion = bedrock.get("name").asText(); + final String newBedrockVersion = bedrock.get("name").getAsString(); // Delayed for two reasons: save unnecessary processing, and wait to load locale if this is on join. GeyserCommandSource sender = recipient.get(); diff --git a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java index 1b7f2d9d966..363c7e3d4bd 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -25,13 +25,19 @@ package org.geysermc.geyser.util; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; @@ -66,17 +72,19 @@ public static String getBody(String reqURL) { } /** - * Makes a web request to the given URL and returns the body as a {@link JsonNode}. + * Makes a web request to the given URL and returns the body as a {@link JsonObject}. * * @param reqURL URL to fetch * @return the response as JSON */ - public static JsonNode getJson(String reqURL) throws IOException { + public static JsonObject getJson(String reqURL) throws IOException { HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection(); con.setRequestProperty("User-Agent", getUserAgent()); con.setConnectTimeout(10000); con.setReadTimeout(10000); - return GeyserImpl.JSON_MAPPER.readTree(con.getInputStream()); + try (JsonReader reader = GeyserImpl.GSON.newJsonReader(new InputStreamReader(con.getInputStream()))) { + return new JsonParser().parse(reader).getAsJsonObject(); + } } /** @@ -194,7 +202,7 @@ public static String postForm(String reqURL, Map fields) throws return ((String) attr.get(0)).split(" "); } } catch (Exception | NoClassDefFoundError ex) { // Check for a NoClassDefFoundError to prevent Android crashes - if (geyser.getConfig().isDebugMode()) { + if (geyser.config().debugMode()) { geyser.getLogger().debug("Exception while trying to find an SRV record for the remote host."); ex.printStackTrace(); // Otherwise we can get a stack trace for any domain that doesn't have an SRV record } @@ -225,6 +233,6 @@ public static Stream getLineStream(String reqURL) { } public static String getUserAgent() { - return "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION; + return "Geyser-" + GeyserImpl.getInstance().platformType().platformName() + "/" + GeyserImpl.VERSION; } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModConfiguration.java b/core/src/main/java/org/geysermc/geyser/util/metrics/MetricsPlatform.java similarity index 59% rename from bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModConfiguration.java rename to core/src/main/java/org/geysermc/geyser/util/metrics/MetricsPlatform.java index a24380bd6a5..bb582df897c 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/util/metrics/MetricsPlatform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,26 +23,26 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.platform.mod; +package org.geysermc.geyser.util.metrics; -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.geysermc.geyser.FloodgateKeyLoader; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.PlatformType; -import java.nio.file.Path; +public interface MetricsPlatform { + boolean enabled(); -public class GeyserModConfiguration extends GeyserJacksonConfiguration { - @JsonIgnore - private Path floodgateKeyPath; + String serverUuid(); - public void loadFloodgate(GeyserModBootstrap geyser, Path floodgateDataFolder) { - Path geyserDataFolder = geyser.getConfigFolder(); + boolean logFailedRequests(); - floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgateDataFolder, geyserDataFolder, geyser.getGeyserLogger()); - } + boolean logSentData(); + + boolean logResponseStatusText(); - @Override - public Path getFloodgateKeyPath() { - return floodgateKeyPath; + default boolean disableRelocateCheck() { + PlatformType platformType = GeyserImpl.getInstance().platformType(); + return platformType == PlatformType.FABRIC || + platformType == PlatformType.NEOFORGE || + platformType == PlatformType.STANDALONE; } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeConfiguration.java b/core/src/main/java/org/geysermc/geyser/util/metrics/ProvidedMetricsPlatform.java similarity index 52% rename from bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeConfiguration.java rename to core/src/main/java/org/geysermc/geyser/util/metrics/ProvidedMetricsPlatform.java index bc084a34e90..c2162bf74b3 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/util/metrics/ProvidedMetricsPlatform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,28 +23,34 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.platform.bungeecord; +package org.geysermc.geyser.util.metrics; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import net.md_5.bungee.api.plugin.Plugin; -import org.geysermc.geyser.FloodgateKeyLoader; -import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; +import org.geysermc.geyser.GeyserImpl; -import java.nio.file.Path; +public final class ProvidedMetricsPlatform implements MetricsPlatform { -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public final class GeyserBungeeConfiguration extends GeyserJacksonConfiguration { - @JsonIgnore - private Path floodgateKeyPath; + @Override + public boolean enabled() { + return GeyserImpl.getInstance().config().enableMetrics(); + } + + @Override + public String serverUuid() { + return GeyserImpl.getInstance().config().advanced().metricsUuid().toString(); + } - public void loadFloodgate(GeyserBungeePlugin plugin) { - Plugin floodgate = plugin.getProxy().getPluginManager().getPlugin("floodgate"); - Path geyserDataFolder = plugin.getDataFolder().toPath(); - Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; + @Override + public boolean logFailedRequests() { + return GeyserImpl.getInstance().config().debugMode(); + } + + @Override + public boolean logSentData() { + return GeyserImpl.getInstance().config().debugMode(); + } - floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgateDataFolder, geyserDataFolder, plugin.getGeyserLogger()); + @Override + public boolean logResponseStatusText() { + return GeyserImpl.getInstance().config().debugMode(); } } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml deleted file mode 100644 index 15d3a20a606..00000000000 --- a/core/src/main/resources/config.yml +++ /dev/null @@ -1,219 +0,0 @@ -# -------------------------------- -# Geyser Configuration File -# -# A bridge between Minecraft: Bedrock Edition and Minecraft: Java Edition. -# -# GitHub: https://github.com/GeyserMC/Geyser -# Discord: https://discord.gg/geysermc -# Wiki: https://wiki.geysermc.org/ -# -# NOTICE: See https://wiki.geysermc.org/geyser/setup/ for the setup guide. Many video tutorials are outdated. -# In most cases, especially with server hosting providers, further hosting-specific configuration is required. -# -------------------------------- - -bedrock: - # The IP address that will listen for connections. - # Generally, you should only uncomment and change this if you want to limit what IPs can connect to your server. - #address: 0.0.0.0 - # The port that will listen for connections - port: 19132 - # Some hosting services change your Java port everytime you start the server and require the same port to be used for Bedrock. - # This option makes the Bedrock port the same as the Java port every time you start the server. - # This option is for the plugin version only. - clone-remote-port: false - # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true - # If either of these are empty, the respective string will default to "Geyser" - motd1: "Geyser" - motd2: "Another Geyser server." - # The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu. - server-name: "Geyser" - # How much to compress network traffic to the Bedrock client. The higher the number, the more CPU usage used, but - # the smaller the bandwidth used. Does not have any effect below -1 or above 9. Set to -1 to disable. - compression-level: 6 - # The port to broadcast to Bedrock clients with the MOTD that they should use to connect to the server. - # DO NOT uncomment and change this unless Geyser runs on a different internal port than the one that is used to connect. - # broadcast-port: 19132 - # Whether to enable PROXY protocol or not for clients. You DO NOT WANT this feature unless you run UDP reverse proxy - # in front of your Geyser instance. - enable-proxy-protocol: false - # A list of allowed PROXY protocol speaking proxy IP addresses/subnets. Only effective when "enable-proxy-protocol" is enabled, and - # should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.). - # Keeping this list empty means there is no IP address whitelist. - # IP addresses, subnets, and links to plain text files are supported. - #proxy-protocol-whitelisted-ips: [ "127.0.0.1", "172.18.0.0/16", "https://example.com/whitelist.txt" ] -remote: - # The IP address of the remote (Java Edition) server - # If it is "auto", for standalone version the remote address will be set to 127.0.0.1, - # for plugin versions, it is recommended to keep this as "auto" so Geyser will automatically configure address, port, and auth-type. - # Leave as "auto" if floodgate is installed. - address: auto - # The port of the remote (Java Edition) server - # For plugin versions, if address has been set to "auto", the port will also follow the server's listening port. - port: 25565 - # Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate). - # For plugin versions, it's recommended to keep the `address` field to "auto" so Floodgate support is automatically configured. - # If Floodgate is installed and `address:` is set to "auto", then "auth-type: floodgate" will automatically be used. - auth-type: online - # Whether to enable PROXY protocol or not while connecting to the server. - # This is useful only when: - # 1) Your server supports PROXY protocol (it probably doesn't) - # 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config. - # IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT! - use-proxy-protocol: false - # Forward the hostname that the Bedrock client used to connect over to the Java server - # This is designed to be used for forced hosts on proxies - forward-hostname: false - -# Floodgate uses encryption to ensure use from authorised sources. -# This should point to the public key generated by Floodgate (BungeeCord, Spigot or Velocity) -# You can ignore this when not using Floodgate. -# If you're using a plugin version of Floodgate on the same server, the key will automatically be picked up from Floodgate. -floodgate-key-file: key.pem - -# For online mode authentication type only. -# Stores a list of Bedrock players that should have their Java Edition account saved after login. -# This saves a token that can be reused to authenticate the player later. This does not save emails or passwords, -# but you should still be cautious when adding to this list and giving others access to this Geyser instance's files. -# Removing a name from this list will delete its cached login information on the next Geyser startup. -# The file that tokens will be saved in is in the same folder as this config, named "saved-refresh-tokens.json". -saved-user-logins: - - ThisExampleUsernameShouldBeLongEnoughToNeverBeAnXboxUsername - - ThisOtherExampleUsernameShouldAlsoBeLongEnough - -# Specify how many seconds to wait while user authorizes Geyser to access their Microsoft account. -# User is allowed to disconnect from the server during this period. -pending-authentication-timeout: 120 - -# Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands. -# Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. -command-suggestions: true - -# The following three options enable "ping passthrough" - the MOTD, player count and/or protocol name gets retrieved from the Java server. -# Relay the MOTD from the remote server to Bedrock players. -passthrough-motd: true -# Relay the player count and max players from the remote server to Bedrock players. -passthrough-player-counts: true -# Enable LEGACY ping passthrough. There is no need to enable this unless your MOTD or player count does not appear properly. -# This option does nothing on standalone. -legacy-ping-passthrough: false -# How often to ping the remote server, in seconds. Only relevant for standalone or legacy ping passthrough. -# Increase if you are getting BrokenPipe errors. -ping-passthrough-interval: 3 - -# Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate -# ping, it may also cause players to time out more easily. -forward-player-ping: false - -# Maximum amount of players that can connect. This is only visual at this time and does not actually limit player count. -max-players: 100 - -# If debug messages should be sent through console -debug-mode: false - -# Allow a fake cooldown indicator to be sent. Bedrock players otherwise do not see a cooldown as they still use 1.8 combat. -# Please note: if the cooldown is enabled, some users may see a black box during the cooldown sequence, like below: -# https://cdn.discordapp.com/attachments/613170125696270357/957075682230419466/Screenshot_from_2022-03-25_20-35-08.png -# This can be disabled by going into Bedrock settings under the accessibility tab and setting "Text Background Opacity" to 0 -# This setting can be set to "title", "actionbar" or "false" -show-cooldown: title - -# Controls if coordinates are shown to players. -show-coordinates: true - -# Whether Bedrock players are blocked from performing their scaffolding-style bridging. -disable-bedrock-scaffolding: false - -# If set, when a Bedrock player performs any emote, it will swap the offhand and mainhand items, just like the Java Edition keybind -# There are three options this can be set to: -# disabled - the default/fallback, which doesn't apply this workaround -# no-emotes - emotes will NOT be sent to other Bedrock clients and offhand will be swapped. This effectively disables all emotes from being seen. -# emotes-and-offhand - emotes will be sent to Bedrock clients and offhand will be swapped -emote-offhand-workaround: "disabled" - -# The default locale if we dont have the one the client requested. Uncomment to not use the default system language. -# default-locale: en_us - -# Specify how many days images will be cached to disk to save downloading them from the internet. -# A value of 0 is disabled. (Default: 0) -cache-images: 0 - -# Allows custom skulls to be displayed. Keeping them enabled may cause a performance decrease on older/weaker devices. -allow-custom-skulls: true - -# The maximum number of custom skulls to be displayed per player. Increasing this may decrease performance on weaker devices. -# Setting this to -1 will cause all custom skulls to be displayed regardless of distance or number. -max-visible-custom-skulls: 128 - -# The radius in blocks around the player in which custom skulls are displayed. -custom-skull-render-distance: 32 - -# Whether to add any items and blocks which normally does not exist in Bedrock Edition. -# This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching. -# If this is disabled, furnace minecart items will be mapped to hopper minecart items. -# Geyser's block, item, and skull mappings systems will also be disabled. -# This option requires a restart of Geyser in order to change its setting. -add-non-bedrock-items: true - -# Bedrock prevents building and displaying blocks above Y127 in the Nether. -# This config option works around that by changing the Nether dimension ID to the End ID. -# The main downside to this is that the entire Nether will have the same red fog rather than having different fog for each biome. -above-bedrock-nether-building: false - -# Force clients to load all resource packs if there are any. -# If set to false, it allows the user to connect to the server even if they don't -# want to download the resource packs. -force-resource-packs: true - -# Allows Xbox achievements to be unlocked. -xbox-achievements-enabled: false - -# Whether player IP addresses will be logged by the server. -log-player-ip-addresses: true - -# Whether to alert the console and operators that a new Geyser version is available that supports a Bedrock version -# that this Geyser version does not support. It's recommended to keep this option enabled, as many Bedrock platforms -# auto-update. -notify-on-new-bedrock-update: true - -# Which item to use to mark unavailable slots in a Bedrock player inventory. Examples of this are the 2x2 crafting grid while in creative, -# or custom inventory menus with sizes different from the usual 3x9. A barrier block is the default item. -unusable-space-block: minecraft:barrier - -# bStats is a stat tracker that is entirely anonymous and tracks only basic information -# about Geyser, such as how many people are online, how many servers are using Geyser, -# what OS is being used, etc. You can learn more about bStats here: https://bstats.org/. -# https://bstats.org/plugin/server-implementation/GeyserMC -metrics: - # If metrics should be enabled - enabled: true - # UUID of server, don't change! - uuid: generateduuid - -# ADVANCED OPTIONS - DO NOT TOUCH UNLESS YOU KNOW WHAT YOU ARE DOING! - -# Geyser updates the Scoreboard after every Scoreboard packet, but when Geyser tries to handle -# a lot of scoreboard packets per second can cause serious lag. -# This option allows you to specify after how many Scoreboard packets per seconds -# the Scoreboard updates will be limited to four updates per second. -scoreboard-packet-threshold: 20 - -# Allow connections from ProxyPass and Waterdog. -# See https://www.spigotmc.org/wiki/firewall-guide/ for assistance - use UDP instead of TCP. -enable-proxy-connections: false - -# The internet supports a maximum MTU of 1492 but could cause issues with packet fragmentation. -# 1400 is the default. -mtu: 1400 - -# Whether to connect directly into the Java server without creating a TCP connection. -# This should only be disabled if a plugin that interfaces with packets or the network does not work correctly with Geyser. -# If enabled on plugin versions, the remote address and port sections are ignored -# If disabled on plugin versions, expect performance decrease and latency increase -use-direct-connection: true - -# Whether Geyser should attempt to disable compression for Bedrock players. This should be a benefit as there is no need to compress data -# when Java packets aren't being handled over the network. -# This requires use-direct-connection to be true. -disable-compression: true - -config-version: 4 diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index e1eafe2c530..57d2045675f 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit e1eafe2c5304012d23acba80659459f7868fe2b1 +Subproject commit 57d2045675fc6f456729a89e39c0e676172510be diff --git a/core/src/test/java/org/geysermc/geyser/configuration/ConfigLoaderTest.java b/core/src/test/java/org/geysermc/geyser/configuration/ConfigLoaderTest.java new file mode 100644 index 00000000000..07242786404 --- /dev/null +++ b/core/src/test/java/org/geysermc/geyser/configuration/ConfigLoaderTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.util.CheckedConsumer; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ConfigLoaderTest { + + @TempDir + Path tempDirectory; + + // Hack until improving ConfigLoader + CommentedConfigurationNode config1; + CommentedConfigurationNode config2; + + @Test + void testCreateNewConfig() throws Exception { + // Test that the result of generating a config, and the result of reading it back after writing it, is the same + + File file = tempDirectory.resolve("config.yml").toFile(); + + forAllConfigs(type -> { + new ConfigLoader(file).transformer(n -> this.config1 = n.copy()).load(type); + long initialModification = file.lastModified(); + assertTrue(file.exists()); // should have been created + List firstContents = Files.readAllLines(file.toPath()); + + new ConfigLoader(file).transformer(n -> this.config2 = n.copy()).load(type); + List secondContents = Files.readAllLines(file.toPath()); + + assertEquals(initialModification, file.lastModified()); // should not have been touched + assertEquals(firstContents, secondContents); + + // Must ignore this, as when the config is read back, the header is interpreted as a comment on the first node in the map + config1.node("java").comment(null); + config2.node("java").comment(null); + assertEquals(config1, config2); + }); + } + + void forAllConfigs(CheckedConsumer, Exception> consumer) throws Exception { + consumer.accept(GeyserRemoteConfig.class); + consumer.accept(GeyserPluginConfig.class); + } +} diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContext.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContext.java index 2b89867fb33..6b0fc4dca9a 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContext.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/GeyserMockContext.java @@ -25,21 +25,22 @@ package org.geysermc.geyser.scoreboard.network.util; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.configuration.GeyserConfiguration; -import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.configuration.AdvancedConfig; +import org.geysermc.geyser.configuration.GeyserConfig; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.mockito.Mockito; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + public class GeyserMockContext { private final List mocksAndSpies = new ArrayList<>(); private final List storedObjects = new ArrayList<>(); @@ -49,11 +50,12 @@ public static void mockContext(Consumer geyserContext) { var context = new GeyserMockContext(); var geyserImpl = context.mock(GeyserImpl.class); - var config = context.mock(GeyserConfiguration.class); + var config = context.mock(GeyserConfig.class); + when(config.advanced()).thenReturn(context.mock(AdvancedConfig.class)); - when(config.getScoreboardPacketThreshold()).thenReturn(1_000); + when(config.advanced().scoreboardPacketThreshold()).thenReturn(1_000); - when(geyserImpl.getConfig()).thenReturn(config); + when(geyserImpl.config()).thenReturn(config); var logger = context.storeObject(new EmptyGeyserLogger()); when(geyserImpl.getLogger()).thenReturn(logger); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f95574ee157..b9761d50005 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,17 @@ [versions] base-api = "1.0.1" +bstats = "3.1.0" cumulus = "1.1.2" +configurate = "4.2.0-GeyserMC-SNAPSHOT" erosion = "1.1-20240521.000109-3" events = "1.1-SNAPSHOT" -jackson = "2.17.0" +yaml = "2.2" fastutil = "8.5.2" netty = "4.1.107.Final" netty-io-uring = "0.0.25.Final-SNAPSHOT" guava = "29.0-jre" -gson = "2.3.1" # Provided by Spigot 1.8.8 +gson = "2.3.1" # Provided by Spigot 1.8.8 TODO bump to 2.8.1 or similar (Spigot 1.16.5 version) after Merge +gson-runtime = "2.10.1" websocket = "1.5.1" protocol-connection = "3.0.0.Beta5-20241022.154658-14" protocol-common = "3.0.0.Beta5-20241022.154658-14" @@ -59,10 +62,11 @@ erosion-bukkit-common = { group = "org.geysermc.erosion", name = "bukkit-common" erosion-bukkit-nms = { group = "org.geysermc.erosion", name = "bukkit-nms", version.ref = "erosion" } erosion-common = { group = "org.geysermc.erosion", name = "common", version.ref = "erosion" } -jackson-annotations = { group = "com.fasterxml.jackson.core", name = "jackson-annotations", version.ref = "jackson" } -jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } -jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-core", version.ref = "jackson" } -jackson-dataformat-yaml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-yaml", version.ref = "jackson" } +yaml = { module = "org.yaml:snakeyaml", version.ref = "yaml" } + +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" } fastutil-int-int-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-int-maps", version.ref = "fastutil" } fastutil-int-long-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-long-maps", version.ref = "fastutil" } @@ -119,6 +123,8 @@ checker-qual = { group = "org.checkerframework", name = "checker-qual", version. commodore = { group = "me.lucko", name = "commodore", version.ref = "commodore" } guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +gson-runtime = { group = "com.google.code.gson", name = "gson", version.ref = "gson-runtime" } +gson-record-factory = { group = "com.github.Marcono1234", name = "gson-record-type-adapter-factory", version = "0.3.0" } junit = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } minecraftauth = { group = "net.raphimc", name = "MinecraftAuth", version.ref = "minecraftauth" } mcprotocollib = { group = "org.geysermc.mcprotocollib", name = "protocol", version.ref = "mcprotocollib" } @@ -135,6 +141,8 @@ protocol-connection = { group = "org.cloudburstmc.protocol", name = "bedrock-con math = { group = "org.cloudburstmc.math", name = "immutable", version = "2.0" } +bstats = { group = "org.bstats", name = "bstats-base", version.ref = "bstats"} + mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" } # plugins @@ -150,7 +158,6 @@ indra = { id = "net.kyori.indra", version.ref = "indra" } blossom = { id = "net.kyori.blossom", version.ref = "blossom" } [bundles] -jackson = [ "jackson-annotations", "jackson-databind", "jackson-dataformat-yaml" ] fastutil = [ "fastutil-int-int-maps", "fastutil-int-long-maps", "fastutil-int-byte-maps", "fastutil-int-boolean-maps", "fastutil-object-int-maps", "fastutil-object-object-maps" ] adventure = [ "adventure-text-serializer-gson", "adventure-text-serializer-legacy", "adventure-text-serializer-plain" ] log4j = [ "log4j-api", "log4j-core", "log4j-slf4j2-impl" ]