diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java new file mode 100644 index 000000000..8d0641599 --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java @@ -0,0 +1,105 @@ +/* + * 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.destroystokyo.paper.event.server.PaperServerListPingEvent; +import com.destroystokyo.paper.network.StatusClient; +import com.destroystokyo.paper.profile.PlayerProfile; +import org.bukkit.Bukkit; +import org.geysermc.geyser.network.GameProtocol; +import org.geysermc.geyser.ping.GeyserPingInfo; +import org.geysermc.geyser.ping.IGeyserPingPassthrough; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.InetSocketAddress; + +/** + * This class is used if possible, so listeners listening for PaperServerListPingEvent exclusively have their changes + * applied. + */ +public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough { + private final GeyserSpigotLogger logger; + + public GeyserPaperPingPassthrough(GeyserSpigotLogger logger) { + this.logger = logger; + } + + @Nullable + @Override + public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) { + try { + // We'd rather *not* use deprecations here, but unfortunately any Adventure class would be relocated at + // runtime because we still have to shade in our own Adventure class. For now. + PaperServerListPingEvent event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress), + Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers(), Bukkit.getVersion(), + GameProtocol.getJavaProtocolVersion(), null); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + // We have to send a ping, so not really sure what else to do here. + return null; + } + + GeyserPingInfo.Players players; + if (event.shouldHidePlayers()) { + players = new GeyserPingInfo.Players(1, 0); + } else { + players = new GeyserPingInfo.Players(event.getMaxPlayers(), event.getNumPlayers()); + } + + GeyserPingInfo geyserPingInfo = new GeyserPingInfo(event.getMotd(), players, + new GeyserPingInfo.Version(Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion())); + + if (!event.shouldHidePlayers()) { + for (PlayerProfile profile : event.getPlayerSample()) { + geyserPingInfo.getPlayerList().add(profile.getName()); + } + } + + return geyserPingInfo; + } catch (Exception e) { + logger.debug("Error while getting Paper ping passthrough: " + e); + return null; + } + } + + private record GeyserStatusClient(InetSocketAddress address) implements StatusClient { + @Override + public @NotNull InetSocketAddress getAddress() { + return address; + } + + @Override + public int getProtocolVersion() { + return GameProtocol.getJavaProtocolVersion(); + } + + @Override + public @Nullable InetSocketAddress getVirtualHost() { + return null; + } + } +} 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 381835b3d..fed5dd6b9 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 @@ -168,8 +168,14 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { if (geyserConfig.isLegacyPingPassthrough()) { this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } else { - this.geyserSpigotPingPassthrough = new GeyserSpigotPingPassthrough(geyserLogger); + try { + Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent"); + this.geyserSpigotPingPassthrough = new GeyserPaperPingPassthrough(geyserLogger); + } catch (ClassNotFoundException e) { + this.geyserSpigotPingPassthrough = new GeyserSpigotPingPassthrough(geyserLogger); + } } + geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass())); this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); this.geyserCommandManager.init(); diff --git a/build-logic/src/main/kotlin/Versions.kt b/build-logic/src/main/kotlin/Versions.kt index c90db32b2..779065bc5 100644 --- a/build-logic/src/main/kotlin/Versions.kt +++ b/build-logic/src/main/kotlin/Versions.kt @@ -33,7 +33,7 @@ object Versions { const val protocolVersion = "977a9a1" const val raknetVersion = "1.6.28-SNAPSHOT" const val mcauthlibVersion = "d9d773e" - const val mcprotocollibversion = "bf3919a" + const val mcprotocollibversion = "bb2b414" const val packetlibVersion = "3.0" const val adventureVersion = "4.9.3" const val eventVersion = "3.0.0" 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 ba900e78d..49261500b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -37,7 +37,6 @@ import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.codec.MinecraftCodecHelper; import com.github.steveice10.mc.protocol.data.ProtocolState; import com.github.steveice10.mc.protocol.data.UnexpectedEncryptionException; -import com.github.steveice10.mc.protocol.data.game.MessageType; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; @@ -123,9 +122,9 @@ import org.geysermc.geyser.session.auth.AuthData; import org.geysermc.geyser.session.auth.BedrockClientData; import org.geysermc.geyser.session.cache.*; import org.geysermc.geyser.skin.FloodgateSkinUploader; +import org.geysermc.geyser.text.ChatTypeEntry; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; -import org.geysermc.geyser.text.TextDecoration; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.ChunkUtils; @@ -336,7 +335,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { */ private final Map dimensions = new Object2ObjectOpenHashMap<>(3); - private final Map chatTypes = new EnumMap<>(MessageType.class); + private final Int2ObjectMap chatTypes = new Int2ObjectOpenHashMap<>(8); @Setter private int breakingBlock; @@ -547,6 +546,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { this.playerEntity = new SessionPlayerEntity(this); collisionManager.updatePlayerBoundingBox(this.playerEntity.getPosition()); + ChatTypeEntry.applyDefaults(chatTypes); + this.playerInventory = new PlayerInventory(); this.openInventory = null; this.craftingRecipes = new Int2ObjectOpenHashMap<>(); diff --git a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java index 060dcc7fb..3250faf64 100644 --- a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java @@ -33,12 +33,15 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import java.net.InetSocketAddress; +import java.util.ArrayDeque; +import java.util.Queue; @RequiredArgsConstructor public class UpstreamSession { @Getter private final BedrockServerSession session; @Getter @Setter private boolean initialized = false; + private Queue postStartGamePackets = new ArrayDeque<>(); public void sendPacket(@NonNull BedrockPacket packet) { if (!isClosed()) { @@ -56,6 +59,25 @@ public class UpstreamSession { session.disconnect(reason); } + /** + * Queue a packet that must be delayed until after login. + */ + public void queuePostStartGamePacket(BedrockPacket packet) { + postStartGamePackets.add(packet); + } + + public void sendPostStartGamePackets() { + if (isClosed()) { + return; + } + + BedrockPacket packet; + while ((packet = postStartGamePackets.poll()) != null) { + session.sendPacket(packet); + } + postStartGamePackets = null; + } + public boolean isClosed() { return session.isClosed(); } diff --git a/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java new file mode 100644 index 000000000..800eb6c0f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java @@ -0,0 +1,57 @@ +/* + * 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.text; + +import com.github.steveice10.mc.protocol.data.game.MessageType; +import com.nukkitx.protocol.bedrock.packet.TextPacket; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public record ChatTypeEntry(@Nonnull TextPacket.Type bedrockChatType, @Nullable TextDecoration textDecoration) { + private static final ChatTypeEntry CHAT = new ChatTypeEntry(TextPacket.Type.CHAT, null); + private static final ChatTypeEntry SYSTEM = new ChatTypeEntry(TextPacket.Type.CHAT, null); + private static final ChatTypeEntry TIP = new ChatTypeEntry(TextPacket.Type.CHAT, null); + private static final ChatTypeEntry RAW = new ChatTypeEntry(TextPacket.Type.CHAT, null); + + /** + * Apply defaults to a map so it isn't empty in the event a chat message is sent before the login packet. + */ + public static void applyDefaults(Int2ObjectMap chatTypes) { + // So the proper way to do this, probably, would be to dump the NBT data from vanilla and load it. + // But, the only way this happens is if a chat message is sent to us before the login packet, which is rare. + // So we'll just make sure chat ends up in the right place. + chatTypes.put(MessageType.CHAT.ordinal(), CHAT); + chatTypes.put(MessageType.SYSTEM.ordinal(), SYSTEM); + chatTypes.put(MessageType.GAME_INFO.ordinal(), TIP); + chatTypes.put(MessageType.SAY_COMMAND.ordinal(), RAW); + chatTypes.put(MessageType.MSG_COMMAND.ordinal(), RAW); + chatTypes.put(MessageType.TEAM_MSG_COMMAND.ordinal(), RAW); + chatTypes.put(MessageType.EMOTE_COMMAND.ordinal(), RAW); + chatTypes.put(MessageType.TELLRAW_COMMAND.ordinal(), RAW); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index 62ae3a302..1c83bf2cc 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -30,16 +30,20 @@ import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLo import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.nukkitx.protocol.bedrock.data.GameRuleData; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.packet.AdventureSettingsPacket; import com.nukkitx.protocol.bedrock.packet.GameRulesChangedPacket; import com.nukkitx.protocol.bedrock.packet.SetPlayerGameTypePacket; +import com.nukkitx.protocol.bedrock.packet.TextPacket; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.ChatTypeEntry; import org.geysermc.geyser.text.TextDecoration; import org.geysermc.geyser.translator.level.BiomeTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -64,21 +68,29 @@ public class JavaLoginTranslator extends PacketTranslator chatTypes = session.getChatTypes(); + Int2ObjectMap chatTypes = session.getChatTypes(); chatTypes.clear(); for (CompoundTag tag : JavaCodecEntry.iterateAsTag(packet.getRegistry().get("minecraft:chat_type"))) { + // The ID is NOT ALWAYS THE SAME! ViaVersion as of 1.19 adds two registry entries that do NOT match vanilla. int id = ((IntTag) tag.get("id")).getValue(); CompoundTag element = tag.get("element"); CompoundTag chat = element.get("chat"); - if (chat == null) { - continue; + TextDecoration textDecoration = null; + if (chat != null) { + CompoundTag decorationTag = chat.get("decoration"); + if (decorationTag != null) { + textDecoration = new TextDecoration(decorationTag); + } } - CompoundTag decoration = chat.get("decoration"); - if (decoration == null) { - continue; - } - MessageType type = MessageType.from(id); - chatTypes.put(type, new TextDecoration(decoration)); + MessageType type = MessageType.from(((StringTag) tag.get("name")).getValue()); + // TODO new types? + TextPacket.Type bedrockType = switch (type) { + case CHAT -> TextPacket.Type.CHAT; + case SYSTEM -> TextPacket.Type.SYSTEM; + case GAME_INFO -> TextPacket.Type.TIP; + default -> TextPacket.Type.RAW; + }; + chatTypes.put(id, new ChatTypeEntry(bedrockType, textDecoration)); } // If the player is already initialized and a join game packet is sent, they @@ -103,6 +115,9 @@ public class JavaLoginTranslator extends PacketTranslator TextPacket.Type.CHAT; - case SYSTEM -> TextPacket.Type.SYSTEM; - case GAME_INFO -> TextPacket.Type.TIP; - default -> TextPacket.Type.RAW; - }); + textPacket.setType(entry.bedrockChatType()); textPacket.setNeedsTranslation(false); Component message = packet.getUnsignedContent() == null ? packet.getSignedContent() : packet.getUnsignedContent(); - TextDecoration decoration = session.getChatTypes().get(packet.getType()); + TextDecoration decoration = entry.textDecoration(); if (decoration != null) { // As of 1.19 - do this to apply all the styling for signed messages // Though, Bedrock cannot care about the signed stuff. diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java index bd49300a8..22942457a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java @@ -41,17 +41,15 @@ public class JavaSystemChatTranslator extends PacketTranslator TextPacket.Type.CHAT; - case SYSTEM -> TextPacket.Type.SYSTEM; - case GAME_INFO -> TextPacket.Type.TIP; - default -> TextPacket.Type.RAW; - }); + textPacket.setType(session.getChatTypes().get(packet.getTypeId()).bedrockChatType()); textPacket.setNeedsTranslation(false); textPacket.setMessage(MessageTranslator.convertMessage(packet.getContent(), session.locale())); - session.sendUpstreamPacket(textPacket); + if (session.isSentSpawnPacket()) { + session.sendUpstreamPacket(textPacket); + } else { + session.getUpstream().queuePostStartGamePacket(textPacket); + } } } diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index e13611fd9..99a1f8070 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit e13611fd97b1801d4c4b914cd409351a49d19537 +Subproject commit 99a1f8070e844d059454dacbb6e8b203521eed23