diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts index 873df692a..9f3b49b67 100644 --- a/bootstrap/bungeecord/build.gradle.kts +++ b/bootstrap/bungeecord/build.gradle.kts @@ -2,6 +2,8 @@ val bungeeVersion = "a7c6ede"; dependencies { api(projects.core) + + implementation("net.kyori", "adventure-text-serializer-bungeecord", Versions.adventurePlatformVersion) } platformRelocate("net.md_5.bungee.jni") 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 new file mode 100644 index 000000000..c68839b20 --- /dev/null +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java @@ -0,0 +1,48 @@ +/* + * 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.bungeecord; + +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource; +import org.geysermc.geyser.util.VersionCheckUtils; + +public final class GeyserBungeeUpdateListener implements Listener { + + @EventHandler + public void onPlayerJoin(final PostLoginEvent event) { + if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + final ProxiedPlayer player = event.getPlayer(); + if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + VersionCheckUtils.checkForGeyserUpdate(() -> new BungeeCommandSource(player)); + } + } + } +} diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java index 801fc8777..f65377643 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java @@ -25,11 +25,15 @@ package org.geysermc.geyser.platform.bungeecord.command; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.ProxiedPlayer; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.GeyserLocale; +import java.util.Locale; + public class BungeeCommandSource implements GeyserCommandSource { private final net.md_5.bungee.api.CommandSender handle; @@ -50,6 +54,18 @@ public class BungeeCommandSource implements GeyserCommandSource { handle.sendMessage(TextComponent.fromLegacyText(message)); } + private static final int PROTOCOL_HEX_COLOR = 713; // Added 20w17a + + @Override + public void sendMessage(Component message) { + if (handle instanceof ProxiedPlayer player && player.getPendingConnection().getVersion() >= PROTOCOL_HEX_COLOR) { + // Include hex colors + handle.sendMessage(BungeeComponentSerializer.get().serialize(message)); + return; + } + handle.sendMessage(BungeeComponentSerializer.legacy().serialize(message)); + } + @Override public boolean isConsole() { return !(handle instanceof ProxiedPlayer); @@ -58,8 +74,11 @@ public class BungeeCommandSource implements GeyserCommandSource { @Override public String locale() { if (handle instanceof ProxiedPlayer player) { - String locale = player.getLocale().getLanguage() + "_" + player.getLocale().getCountry(); - return GeyserLocale.formatLocale(locale); + Locale locale = player.getLocale(); + if (locale != null) { + // Locale can be null early on in the conneciton + return GeyserLocale.formatLocale(locale.getLanguage() + "_" + locale.getCountry()); + } } return GeyserLocale.getDefaultLocale(); } diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index 02883999d..5a459a09b 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -1,7 +1,7 @@ val paperVersion = "1.19-R0.1-SNAPSHOT" val viaVersion = "4.0.0" val adaptersVersion = "1.5-SNAPSHOT" -val commodoreVersion = "1.13" +val commodoreVersion = "2.2" dependencies { api(projects.core) @@ -9,6 +9,8 @@ dependencies { implementation("org.geysermc.geyser.adapters", "spigot-all", adaptersVersion) implementation("me.lucko", "commodore", commodoreVersion) + + implementation("net.kyori", "adventure-text-serializer-bungeecord", Versions.adventurePlatformVersion) // Both paper-api and paper-mojangapi only provide Java 17 versions for 1.19 compileOnly("io.papermc.paper", "paper-api", paperVersion) { @@ -25,7 +27,8 @@ dependencies { platformRelocate("it.unimi.dsi.fastutil") platformRelocate("com.fasterxml.jackson") -platformRelocate("net.kyori") +// 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("io.netty.channel.kqueue") diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java new file mode 100644 index 000000000..930f84cec --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java @@ -0,0 +1,59 @@ +/* + * 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 net.kyori.adventure.text.Component; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import org.bukkit.plugin.Plugin; + +import java.util.logging.Logger; + +public final class GeyserPaperLogger extends GeyserSpigotLogger { + private final ComponentLogger componentLogger; + + public GeyserPaperLogger(Plugin plugin, Logger logger, boolean debug) { + super(logger, debug); + componentLogger = plugin.getComponentLogger(); + } + + /** + * Since 1.18.2 this is required so legacy format symbols don't show up in the console for colors + */ + @Override + public void sendMessage(Component message) { + // Done like this so the native component object field isn't relocated + componentLogger.info("{}", PaperAdventure.toNativeComponent(message)); + } + + static boolean supported() { + try { + Plugin.class.getMethod("getComponentLogger"); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } +} 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 0f89aa682..4a371cfe8 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 @@ -123,6 +123,22 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return; } + try { + Class.forName("net.md_5.bungee.chat.ComponentSerializer"); + } catch (ClassNotFoundException e) { + if (!PaperAdventure.canSendMessageUsingComponent()) { // Prepare for Paper eventually removing Bungee chat + getLogger().severe("*********************************************"); + getLogger().severe(""); + getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.header", getServer().getName())); + getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper")); + getLogger().severe(""); + getLogger().severe("*********************************************"); + + Bukkit.getPluginManager().disablePlugin(this); + return; + } + } + // By default this should be localhost but may need to be changed in some circumstances if (this.geyserConfig.getRemote().address().equalsIgnoreCase("auto")) { geyserConfig.setAutoconfiguredRemote(true); @@ -137,7 +153,8 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserConfig.getBedrock().setPort(Bukkit.getPort()); } - this.geyserLogger = new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode()); + this.geyserLogger = GeyserPaperLogger.supported() ? new GeyserPaperLogger(this, getLogger(), geyserConfig.isDebugMode()) + : new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); // Remove this in like a year @@ -269,12 +286,16 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { GeyserLocale.getLocaleStringLog(command.description()), command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE)); } + Bukkit.getPluginManager().addPermission(new Permission(Constants.UPDATE_PERMISSION, + "Whether update notifications can be seen", PermissionDefault.OP)); // Events cannot be unregistered - re-registering results in duplicate firings GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager); Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this); Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this); + + Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigotUpdateListener(), this); } boolean brigadierSupported = CommodoreProvider.isSupported(); 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 new file mode 100644 index 000000000..5e3c4def8 --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java @@ -0,0 +1,48 @@ +/* + * 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 org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource; +import org.geysermc.geyser.util.VersionCheckUtils; + +public final class GeyserSpigotUpdateListener implements Listener { + + @EventHandler + public void onPlayerJoin(final PlayerJoinEvent event) { + if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + final Player player = event.getPlayer(); + if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + VersionCheckUtils.checkForGeyserUpdate(() -> new SpigotCommandSource(player)); + } + } + } +} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java new file mode 100644 index 000000000..5dd16da33 --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java @@ -0,0 +1,154 @@ +/* + * 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.github.steveice10.mc.protocol.data.DefaultComponentSerializer; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import org.geysermc.geyser.GeyserImpl; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Utility class for converting our shaded Adventure into the Adventure bundled in Paper. + * + * Code mostly taken from https://github.com/KyoriPowered/adventure-platform/blob/94d5821f2e755170f42bd8a5fe1d5bf6f66d04ad/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/PaperFacet.java#L46 + * and the MinecraftReflection class. + */ +public final class PaperAdventure { + private static final MethodHandle NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND; + private static final Method SEND_MESSAGE_COMPONENT; + + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + + MethodHandle nativeGsonComponentSerializerDeserializeMethodBound = null; + + // String.join because otherwise the class name will be relocated + final Class nativeGsonComponentSerializerClass = findClass(String.join(".", + "net", "kyori", "adventure", "text", "serializer", "gson", "GsonComponentSerializer")); + final Class nativeGsonComponentSerializerImplClass = findClass(String.join(".", + "net", "kyori", "adventure", "text", "serializer", "gson", "GsonComponentSerializerImpl")); + if (nativeGsonComponentSerializerClass != null && nativeGsonComponentSerializerImplClass != null) { + MethodHandle nativeGsonComponentSerializerGsonGetter = null; + try { + nativeGsonComponentSerializerGsonGetter = lookup.findStatic(nativeGsonComponentSerializerClass, + "gson", MethodType.methodType(nativeGsonComponentSerializerClass)); + } catch (final NoSuchMethodException | IllegalAccessException ignored) { + } + + MethodHandle nativeGsonComponentSerializerDeserializeMethod = null; + try { + final Method method = nativeGsonComponentSerializerImplClass.getDeclaredMethod("deserialize", String.class); + method.setAccessible(true); + nativeGsonComponentSerializerDeserializeMethod = lookup.unreflect(method); + } catch (final NoSuchMethodException | IllegalAccessException ignored) { + } + + if (nativeGsonComponentSerializerGsonGetter != null) { + if (nativeGsonComponentSerializerDeserializeMethod != null) { + try { + nativeGsonComponentSerializerDeserializeMethodBound = nativeGsonComponentSerializerDeserializeMethod + .bindTo(nativeGsonComponentSerializerGsonGetter.invoke()); + } catch (final Throwable throwable) { + GeyserImpl.getInstance().getLogger().error("Failed to access native GsonComponentSerializer", throwable); + } + } + } + } + + NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND = nativeGsonComponentSerializerDeserializeMethodBound; + + Method playerComponentSendMessage = null; + final Class nativeComponentClass = findClass(String.join(".", + "net", "kyori", "adventure", "text", "Component")); + if (nativeComponentClass != null) { + try { + playerComponentSendMessage = CommandSender.class.getMethod("sendMessage", nativeComponentClass); + } catch (final NoSuchMethodException e) { + if (GeyserImpl.getInstance().getLogger().isDebug()) { + e.printStackTrace(); + } + } + } + SEND_MESSAGE_COMPONENT = playerComponentSendMessage; + } + + public static Object toNativeComponent(final Component component) { + if (NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND == null) { + GeyserImpl.getInstance().getLogger().error("Illegal state where Component serialization was called when it wasn't available!"); + return null; + } + + try { + return NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND.invoke(DefaultComponentSerializer.get().serialize(component)); + } catch (final Throwable throwable) { + GeyserImpl.getInstance().getLogger().error("Failed to create native Component message", throwable); + return null; + } + } + + public static void sendMessage(final CommandSender sender, final Component component) { + if (SEND_MESSAGE_COMPONENT == null) { + GeyserImpl.getInstance().getLogger().error("Illegal state where Component sendMessage was called when it wasn't available!"); + return; + } + + final Object nativeComponent = toNativeComponent(component); + if (nativeComponent != null) { + try { + SEND_MESSAGE_COMPONENT.invoke(sender, nativeComponent); + } catch (final InvocationTargetException | IllegalAccessException e) { + GeyserImpl.getInstance().getLogger().error("Failed to send native Component message", e); + } + } + } + + public static boolean canSendMessageUsingComponent() { + return SEND_MESSAGE_COMPONENT != null; + } + + /** + * Gets a class by the first name available. + * + * @return a class or {@code null} if not found + */ + private static @Nullable Class findClass(final String className) { + try { + return Class.forName(className); + } catch (final ClassNotFoundException ignored) { + } + return null; + } + + private PaperAdventure() { + } +} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java index 839e4d88e..2a54ad440 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java @@ -25,10 +25,13 @@ package org.geysermc.geyser.platform.spigot.command; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.platform.spigot.PaperAdventure; import org.geysermc.geyser.text.GeyserLocale; import java.lang.reflect.InvocationTargetException; @@ -63,6 +66,16 @@ public class SpigotCommandSource implements GeyserCommandSource { handle.sendMessage(message); } + @Override + public void sendMessage(Component message) { + if (PaperAdventure.canSendMessageUsingComponent()) { + PaperAdventure.sendMessage(handle, message); + return; + } + + handle.sendMessage(BungeeComponentSerializer.get().serialize(message)); + } + @Override public boolean isConsole() { return handle instanceof ConsoleCommandSender; 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 31b395a61..e7e24a465 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 @@ -95,24 +95,4 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey public boolean isDebug() { return log.isDebugEnabled(); } - - @Override - public String name() { - return "CONSOLE"; - } - - @Override - public void sendMessage(String message) { - info(message); - } - - @Override - public boolean isConsole() { - return true; - } - - @Override - public boolean hasPermission(String permission) { - return true; - } } 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 c91cbb2ca..e2f84a2a7 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 @@ -165,6 +165,8 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { } else { this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer); } + + proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener()); } @Override 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 new file mode 100644 index 000000000..31e584612 --- /dev/null +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java @@ -0,0 +1,47 @@ +/* + * 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.velocity; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.proxy.Player; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource; +import org.geysermc.geyser.util.VersionCheckUtils; + +public final class GeyserVelocityUpdateListener { + + @Subscribe + public void onPlayerJoin(PostLoginEvent event) { + if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + final Player player = event.getPlayer(); + if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + VersionCheckUtils.checkForGeyserUpdate(() -> new VelocityCommandSource(player)); + } + } + } +} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java index fa70d1cf7..00c99e92b 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.platform.velocity.command; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.GeyserLocale; @@ -59,6 +60,12 @@ public class VelocityCommandSource implements GeyserCommandSource { handle.sendMessage(LegacyComponentSerializer.legacy('ยง').deserialize(message)); } + @Override + public void sendMessage(Component message) { + // Be careful that we don't shade in Adventure!! + handle.sendMessage(message); + } + @Override public boolean isConsole() { return handle instanceof ConsoleCommandSource; diff --git a/build-logic/src/main/kotlin/Versions.kt b/build-logic/src/main/kotlin/Versions.kt index f7b20cca1..d4a0a80e3 100644 --- a/build-logic/src/main/kotlin/Versions.kt +++ b/build-logic/src/main/kotlin/Versions.kt @@ -39,6 +39,7 @@ object Versions { const val mcprotocollibversion = "9f78bd5" const val packetlibVersion = "3.0" const val adventureVersion = "4.9.3" + const val adventurePlatformVersion = "4.1.2" const val junitVersion = "4.13.1" const val checkerQualVersion = "3.19.0" const val cumulusVersion = "1.1.1" diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt index 1f9793ee4..43cdafdcc 100644 --- a/build-logic/src/main/kotlin/extensions.kt +++ b/build-logic/src/main/kotlin/extensions.kt @@ -43,9 +43,11 @@ fun Project.exclude(group: String) { } } -fun Project.platformRelocate(pattern: String) { +fun Project.platformRelocate(pattern: String, exclusion: String = "") { tasks.named("shadowJar") { - relocate(pattern, "org.geysermc.geyser.platform.${project.name}.shaded.$pattern") + relocate(pattern, "org.geysermc.geyser.platform.${project.name}.shaded.$pattern") { + exclude(exclusion) + } } } diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 23fb76d16..6a53c37de 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -37,6 +37,9 @@ public final class Constants { public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/"; + public static final String GEYSER_DOWNLOAD_LOCATION = "https://ci.geysermc.org"; + public static final String UPDATE_PERMISSION = "geyser.update"; + static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; static { diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 87a40c828..ed65ffbe9 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -41,6 +41,8 @@ import io.netty.util.internal.SystemPropertyUtil; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -77,7 +79,6 @@ import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.session.SessionManager; import org.geysermc.geyser.skin.FloodgateSkinUploader; import org.geysermc.geyser.skin.SkinProvider; -import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; @@ -312,8 +313,8 @@ public class GeyserImpl implements GeyserApi { int port = config.getBedrock().port(); logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, String.valueOf(port))); if (!"0.0.0.0".equals(address)) { - logger.info(ChatColor.GREEN + "Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0"); - logger.info(ChatColor.GREEN + "Then, restart this server."); + logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN)); + logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN)); } } }).join(); @@ -465,6 +466,9 @@ public class GeyserImpl implements GeyserApi { newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED); this.eventBus.fire(new GeyserPostInitializeEvent(this.extensionManager, this.eventBus)); + if (config.isNotifyOnNewBedrockUpdate()) { + VersionCheckUtils.checkForGeyserUpdate(this::getLogger); + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index b47801cb5..88220eec9 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -25,9 +25,12 @@ package org.geysermc.geyser; +import net.kyori.adventure.text.Component; +import org.geysermc.geyser.command.GeyserCommandSource; + import javax.annotation.Nullable; -public interface GeyserLogger { +public interface GeyserLogger extends GeyserCommandSource { /** * Logs a severe message to console @@ -73,6 +76,15 @@ public interface GeyserLogger { */ void info(String message); + /** + * Logs an info component to console + * + * @param message the message to log + */ + default void info(Component message) { + sendMessage(message); + } + /** * Logs a debug message to console * @@ -100,4 +112,24 @@ public interface GeyserLogger { * If debug is enabled for this logger */ boolean isDebug(); + + @Override + default String name() { + return "CONSOLE"; + } + + @Override + default void sendMessage(String message) { + info(message); + } + + @Override + default boolean isConsole() { + return true; + } + + @Override + default boolean hasPermission(String permission) { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java index eabccc243..88d148b11 100644 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java +++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java @@ -27,6 +27,8 @@ package org.geysermc.geyser.command; import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.text.GeyserLocale; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; /** * Implemented on top of any class that can send a command. @@ -40,4 +42,8 @@ public interface GeyserCommandSource extends CommandSource { default String locale() { return GeyserLocale.getDefaultLocale(); } + + default void sendMessage(Component message) { + sendMessage(LegacyComponentSerializer.legacySection().serialize(message)); + } } diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java index 7439b22f2..355080a3a 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -107,6 +107,8 @@ public interface GeyserConfiguration { int getCustomSkullRenderDistance(); + boolean isNotifyOnNewBedrockUpdate(); + IMetricsInfo getMetrics(); int getPendingAuthenticationTimeout(); diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java index b80e60e49..9f2eaa898 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -148,6 +148,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("xbox-achievements-enabled") private boolean xboxAchievementsEnabled = false; + @JsonProperty("notify-on-new-bedrock-update") + private boolean notifyOnNewBedrockUpdate = true; + private MetricsInfo metrics = new MetricsInfo(); @JsonProperty("pending-authentication-timeout") diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 2550643d3..f7e055417 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -106,6 +106,9 @@ public class LivingEntity extends Entity { // Riptide spin attack setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, (xd & 0x04) == 0x04); + + // OptionalPack usage + setFlag(EntityFlag.EMERGING, isUsingItem && isUsingOffhand); } public void setHealth(FloatEntityMetadata entityMetadata) { diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 9f652de9a..401f5b009 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -45,7 +45,10 @@ public final class GameProtocol { * Default Bedrock codec that should act as a fallback. Should represent the latest available * release of the game that Geyser supports. */ - public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v544.V544_CODEC; + public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v544.V544_CODEC.toBuilder() + .minecraftVersion("1.19.21") + .protocolVersion(545) + .build(); /** * A list of all supported Bedrock versions that can join Geyser */ @@ -64,6 +67,7 @@ public final class GameProtocol { SUPPORTED_BEDROCK_CODECS.add(Bedrock_v534.V534_CODEC.toBuilder() .minecraftVersion("1.19.10/1.19.11") .build()); + SUPPORTED_BEDROCK_CODECS.add(Bedrock_v544.V544_CODEC); SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java index 239f5c865..30f8c3ba8 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java @@ -31,6 +31,7 @@ import com.nukkitx.protocol.bedrock.packet.SetTitlePacket; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; import lombok.Setter; +import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.scoreboard.Scoreboard; import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession; import org.geysermc.geyser.session.GeyserSession; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 521adb687..e72f7d786 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -54,6 +54,7 @@ import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.click.Click; import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; @@ -62,10 +63,7 @@ import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; -import org.geysermc.geyser.util.BlockUtils; -import org.geysermc.geyser.util.EntityUtils; -import org.geysermc.geyser.util.InteractionResult; -import org.geysermc.geyser.util.InventoryUtils; +import org.geysermc.geyser.util.*; import java.util.List; import java.util.concurrent.TimeUnit; @@ -468,6 +466,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator 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(); + if (GameProtocol.getBedrockCodec(protocolVersion) != null) { + // We support the latest version! No need to print a message. + return; + } + + final String newBedrockVersion = bedrock.get("name").asText(); + + // Delayed for two reasons: save unnecessary processing, and wait to load locale if this is on join. + GeyserCommandSource sender = recipient.get(); + + // Overarching component is green - geyser.version.new component cannot be green or else the link blue is overshadowed + Component message = Component.text().color(NamedTextColor.GREEN) + .append(Component.text(GeyserLocale.getPlayerLocaleString("geyser.version.new", sender.locale(), newBedrockVersion)) + .replaceText(TextReplacementConfig.builder() + .match("\\{1\\}") // Replace "Download here: {1}" so we can use fancy text component yesyes + .replacement(Component.text() + .content(Constants.GEYSER_DOWNLOAD_LOCATION) + .color(NamedTextColor.BLUE) + .decoration(TextDecoration.UNDERLINED, TextDecoration.State.TRUE) + .clickEvent(ClickEvent.openUrl(Constants.GEYSER_DOWNLOAD_LOCATION))) + .build())) + .build(); + sender.sendMessage(message); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Error whilst checking for Geyser update!", e); + } + }); + } + private VersionCheckUtils() { } } 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 f9574f08b..c0889f1c5 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -73,6 +73,8 @@ public class WebUtils { public static JsonNode getJson(String reqURL) throws IOException { HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection(); con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().toString() + "/" + GeyserImpl.VERSION); + con.setConnectTimeout(10000); + con.setReadTimeout(10000); return GeyserImpl.JSON_MAPPER.readTree(con.getInputStream()); } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index c331a7e62..5a32a6599 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -175,6 +175,11 @@ force-resource-packs: true # THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating. xbox-achievements-enabled: false +# 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 + # 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/.