diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index fceed8498..000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,28 +0,0 @@ -pipeline { - agent any - tools { - gradle 'Gradle 7' - jdk 'Java 17' - } - options { - buildDiscarder(logRotator(artifactNumToKeepStr: '20')) - } - stages { - stage ('Build') { - steps { - sh 'git submodule update --init --recursive' - rtGradleRun( - usesPlugin: true, - tool: 'Gradle 7', - buildFile: 'build.gradle.kts', - tasks: 'clean build', - ) - } - post { - success { - archiveArtifacts artifacts: 'bootstrap/**/build/libs/Geyser-*.jar', fingerprint: true - } - } - } - } -} diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java index 036723092..63788df8e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java +++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java @@ -185,7 +185,7 @@ public interface CustomBlockComponents { Builder placeAir(boolean placeAir); - Builder tags(Set tags); + Builder tags(@Nullable Set tags); CustomBlockComponents build(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java new file mode 100644 index 000000000..05e3415a0 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java @@ -0,0 +1,61 @@ +/* + * 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.api.event.bedrock; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.connection.ConnectionEvent; + +/** + * Called when a Geyser session disconnects. + */ +public class SessionDisconnectEvent extends ConnectionEvent { + private String disconnectReason; + + public SessionDisconnectEvent(@NonNull GeyserConnection connection, @NonNull String reason) { + super(connection); + this.disconnectReason = reason; + } + + /** + * Gets the disconnect reason. + * + * @return the reason for the disconnect + */ + public @NonNull String disconnectReason() { + return disconnectReason; + } + + /** + * Sets the disconnect reason, thereby overriding th original reason. + * + * @param disconnectReason the reason for the disconnect + */ + public void disconnectReason(@NonNull String disconnectReason) { + this.disconnectReason = disconnectReason; + } +} + diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java index d256b9ac0..404679e60 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java @@ -29,6 +29,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.GeyserApi; +import java.util.Set; + /** * This is used to store data for a custom item. */ @@ -89,6 +91,14 @@ public interface CustomItemData { */ @Nullable CustomRenderOffsets renderOffsets(); + /** + * Gets the item's set of tags that can be used in Molang. + * Equivalent to "tag:some_tag" + * + * @return the item's tags, if they exist + */ + @NonNull Set tags(); + static CustomItemData.Builder builder() { return GeyserApi.api().provider(CustomItemData.Builder.class); } @@ -113,6 +123,8 @@ public interface CustomItemData { Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets); + Builder tags(@Nullable Set tags); + CustomItemData build(); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java index 0a09f6958..616a5bba6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java @@ -239,6 +239,9 @@ public interface NonVanillaCustomItemData extends CustomItemData { @Override Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets); + @Override + Builder tags(@Nullable Set tags); + NonVanillaCustomItemData build(); } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java index 084e1d2dc..485079a05 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java @@ -32,18 +32,26 @@ import net.md_5.bungee.protocol.packet.LoginSuccess; import net.md_5.bungee.protocol.packet.SetCompression; public class GeyserBungeeCompressionDisabler extends ChannelOutboundHandlerAdapter { + private boolean compressionDisabled = false; @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (!(msg instanceof SetCompression)) { - if (msg instanceof LoginSuccess) { - // We're past the point that compression can be enabled + // Fixes https://github.com/GeyserMC/Geyser/issues/4281 + // The server may send a LoginDisconnect packet after compression is set. + if (!compressionDisabled) { if (ctx.pipeline().get("compress") != null) { ctx.pipeline().remove("compress"); + compressionDisabled = true; } if (ctx.pipeline().get("decompress") != null) { ctx.pipeline().remove("decompress"); + compressionDisabled = true; } + } + + if (msg instanceof LoginSuccess) { + // We're past the point that compression can be enabled ctx.pipeline().remove(this); } super.write(ctx, msg, promise); 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 39fb9e134..e14e8ff66 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 @@ -40,7 +40,6 @@ import org.geysermc.geyser.ping.IGeyserPingPassthrough; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.util.Arrays; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -61,16 +60,11 @@ public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, List })); ProxyPingEvent event = future.join(); ServerPing response = event.getResponse(); - GeyserPingInfo geyserPingInfo = new GeyserPingInfo( + return new GeyserPingInfo( response.getDescriptionComponent().toLegacyText(), - new GeyserPingInfo.Players(response.getPlayers().getMax(), response.getPlayers().getOnline()), - new GeyserPingInfo.Version(response.getVersion().getName(), response.getVersion().getProtocol()) + response.getPlayers().getMax(), + response.getPlayers().getOnline() ); - if (event.getResponse().getPlayers().getSample() != null) { - Arrays.stream(event.getResponse().getPlayers().getSample()).forEach(proxiedPlayer -> - geyserPingInfo.getPlayerList().add(proxiedPlayer.getName())); - } - return geyserPingInfo; } // This is static so pending connection can use it diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java index 0bbe73f16..af0c9efca 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java @@ -144,7 +144,11 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap { GeyserImpl.start(); - this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); + if (geyserConfig.isLegacyPingPassthrough()) { + this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); + } else { + this.geyserPingPassthrough = new ModPingPassthrough(server, geyserLogger); + } this.geyserCommandManager = new GeyserCommandManager(geyser); this.geyserCommandManager.init(); diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/ModPingPassthrough.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/ModPingPassthrough.java new file mode 100644 index 000000000..e74be7fb7 --- /dev/null +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/ModPingPassthrough.java @@ -0,0 +1,110 @@ +/* + * 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.fabric; + +import lombok.AllArgsConstructor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket; +import net.minecraft.network.protocol.status.ServerStatus; +import net.minecraft.network.protocol.status.ServerStatusPacketListener; +import net.minecraft.network.protocol.status.ServerboundStatusRequestPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerStatusPacketListenerImpl; +import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.ping.GeyserPingInfo; +import org.geysermc.geyser.ping.IGeyserPingPassthrough; +import org.jetbrains.annotations.Nullable; + +import java.net.InetSocketAddress; +import java.util.Objects; + +@AllArgsConstructor +public class ModPingPassthrough implements IGeyserPingPassthrough { + + private static final GsonComponentSerializer GSON_SERIALIZER = GsonComponentSerializer.gson(); + private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection(); + + private final MinecraftServer server; + private final GeyserLogger logger; + + @Nullable + @Override + public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) { + ServerStatus status = server.getStatus(); + if (status == null) { + return null; + } + + try { + StatusInterceptor connection = new StatusInterceptor(); + ServerStatusPacketListener statusPacketListener = new ServerStatusPacketListenerImpl(status, connection); + + statusPacketListener.handleStatusRequest(new ServerboundStatusRequestPacket()); + // mods like MiniMOTD (that inject into the above method) have now processed the response + status = Objects.requireNonNull(connection.status, "status response"); + } catch (Exception e) { + if (logger.isDebug()) { + logger.debug("Failed to listen for modified ServerStatus: " + e.getMessage()); + e.printStackTrace(); + } + } + + String jsonDescription = net.minecraft.network.chat.Component.Serializer.toJson(status.description()); + String legacyDescription = LEGACY_SERIALIZER.serialize(GSON_SERIALIZER.deserializeOr(jsonDescription, Component.empty())); + + return new GeyserPingInfo( + legacyDescription, + status.players().map(ServerStatus.Players::max).orElse(1), + status.players().map(ServerStatus.Players::online).orElse(0) + ); + } + + /** + * Custom Connection that intercepts the status response right before it is sent + */ + private static class StatusInterceptor extends Connection { + + ServerStatus status; + + StatusInterceptor() { + super(PacketFlow.SERVERBOUND); // we are the server. + } + + @Override + public void send(Packet packet, @Nullable PacketSendListener packetSendListener, boolean bl) { + if (packet instanceof ClientboundStatusResponsePacket statusResponse) { + status = statusResponse.status(); + } + super.send(packet, packetSendListener, bl); + } + } +} 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 index bb0f30e70..60e0ae519 100644 --- 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 @@ -27,7 +27,6 @@ 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; @@ -81,16 +80,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough 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; + return new GeyserPingInfo(event.getMotd(), players); } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change logger.debug("Error while getting Paper ping passthrough: " + e); return null; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java index 1e6a0ad6c..df197f137 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java @@ -30,7 +30,6 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.server.ServerListPingEvent; import org.bukkit.util.CachedServerIcon; -import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserPingInfo; import org.geysermc.geyser.ping.IGeyserPingPassthrough; @@ -50,12 +49,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough { try { ServerListPingEvent event = new GeyserPingEvent(inetSocketAddress.getAddress(), Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers()); Bukkit.getPluginManager().callEvent(event); - GeyserPingInfo geyserPingInfo = new GeyserPingInfo(event.getMotd(), - new GeyserPingInfo.Players(event.getMaxPlayers(), event.getNumPlayers()), - new GeyserPingInfo.Version(Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion()) // thanks Spigot for not exposing this, just default to latest - ); - Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add); - return geyserPingInfo; + return new GeyserPingInfo(event.getMotd(), event.getMaxPlayers(), event.getNumPlayers()); } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change logger.debug("Error while getting Bukkit ping passthrough: " + e); return null; diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java index 1a9b9bf26..8944ea134 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java @@ -54,19 +54,11 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } - GeyserPingInfo geyserPingInfo = new GeyserPingInfo( + return new GeyserPingInfo( LegacyComponentSerializer.legacy('ยง').serialize(event.getPing().getDescriptionComponent()), - new GeyserPingInfo.Players( - event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getMax(), - event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getOnline() - ), - new GeyserPingInfo.Version( - event.getPing().getVersion().getName(), - event.getPing().getVersion().getProtocol() - ) + event.getPing().getPlayers().map(ServerPing.Players::getMax).orElse(1), + event.getPing().getPlayers().map(ServerPing.Players::getOnline).orElse(0) ); - event.getPing().getPlayers().get().getSample().stream().map(ServerPing.SamplePlayer::getName).forEach(geyserPingInfo.getPlayerList()::add); - return geyserPingInfo; } private static class GeyserInboundConnection implements InboundConnection { 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 93b57d712..d7c6c588e 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 @@ -206,9 +206,9 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { // After this bound, we know that the channel initializer cannot change without it being ineffective for Velocity, too geyserInjector.initializeLocalChannel(this); } - } - INITIALIZED = true; + INITIALIZED = true; + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java index e4baeebb5..1827cfb36 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java @@ -25,6 +25,8 @@ package org.geysermc.geyser; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; @@ -32,8 +34,6 @@ import org.geysermc.geyser.level.GeyserWorldManager; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.InputStream; import java.net.SocketAddress; import java.nio.file.Path; @@ -79,6 +79,7 @@ public interface GeyserBootstrap { * * @return The current PingPassthrough manager */ + @Nullable IGeyserPingPassthrough getGeyserPingPassthrough(); /** @@ -151,7 +152,7 @@ public interface GeyserBootstrap { * @param resource Resource to get * @return InputStream of the given resource */ - default @Nonnull InputStream getResource(String resource) { + default @NonNull InputStream getResource(String resource) { InputStream stream = getResourceOrNull(resource); if (stream == null) { throw new AssertionError("Unable to find resource: " + resource); @@ -162,7 +163,7 @@ public interface GeyserBootstrap { /** * @return the bind address being used by the Java server. */ - @Nonnull + @NonNull String getServerBindAddress(); /** diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java index 8f147cdab..a3cd8fa4c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java @@ -32,6 +32,8 @@ import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; +import java.util.concurrent.TimeUnit; + public class ReloadCommand extends GeyserCommand { private final GeyserImpl geyser; @@ -52,7 +54,8 @@ public class ReloadCommand extends GeyserCommand { sender.sendMessage(message); geyser.getSessionManager().disconnectAll("geyser.commands.reload.kick"); - geyser.reload(); + //FIXME Without the tiny wait, players do not get kicked - same happens when Geyser tries to disconnect all sessions on shutdown + geyser.getScheduledThread().schedule(geyser::reload, 10, TimeUnit.MILLISECONDS); } @Override 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 e36ec819b..51cd7d4ae 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -57,9 +57,6 @@ public interface GeyserConfiguration { @JsonIgnore boolean isPassthroughMotd(); - @JsonIgnore - boolean isPassthroughProtocolName(); - @JsonIgnore boolean isPassthroughPlayerCounts(); 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 268304844..95ac7b1de 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -75,9 +75,6 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("passthrough-player-counts") private boolean isPassthroughPlayerCounts = false; - @JsonProperty("passthrough-protocol-name") - private boolean isPassthroughProtocolName = false; - @JsonProperty("legacy-ping-passthrough") private boolean isLegacyPingPassthrough = false; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java index f2671f7a9..703d5210b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.entity.type; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import net.kyori.adventure.text.Component; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -49,6 +50,12 @@ public class TextDisplayEntity extends Entity { this.dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); } + @Override + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + // Don't allow the display name to be hidden - messes with our armor stand. + // On JE: Hiding the display name still shows the display entity text. + } + public void setText(EntityMetadata entityMetadata) { this.dirtyMetadata.put(EntityDataTypes.NAME, MessageTranslator.convertMessage(entityMetadata.getValue())); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index bcd32253f..58b7ffddb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -254,4 +254,17 @@ public class SessionPlayerEntity extends PlayerEntity { public UUID getTabListUuid() { return session.getAuthData().uuid(); } + + public void resetMetadata() { + // Reset all metadata to their default values + // This is used when a player respawns + this.initializeMetadata(); + + // Reset air + this.resetAir(); + } + + public void resetAir() { + this.setAirSupply(getMaxAir()); + } } diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java index 3535eaf4d..c86c370bb 100644 --- a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java @@ -28,10 +28,14 @@ package org.geysermc.geyser.item; import lombok.EqualsAndHashCode; import lombok.ToString; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; -import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; @EqualsAndHashCode @ToString @@ -44,6 +48,7 @@ public class GeyserCustomItemData implements CustomItemData { private final boolean displayHandheld; private final int textureSize; private final CustomRenderOffsets renderOffsets; + private final Set tags; public GeyserCustomItemData(String name, CustomItemOptions customItemOptions, @@ -52,7 +57,8 @@ public class GeyserCustomItemData implements CustomItemData { boolean allowOffhand, boolean displayHandheld, int textureSize, - CustomRenderOffsets renderOffsets) { + CustomRenderOffsets renderOffsets, + Set tags) { this.name = name; this.customItemOptions = customItemOptions; this.displayName = displayName; @@ -61,10 +67,11 @@ public class GeyserCustomItemData implements CustomItemData { this.displayHandheld = displayHandheld; this.textureSize = textureSize; this.renderOffsets = renderOffsets; + this.tags = tags; } @Override - public @NotNull String name() { + public @NonNull String name() { return name; } @@ -74,12 +81,12 @@ public class GeyserCustomItemData implements CustomItemData { } @Override - public @NotNull String displayName() { + public @NonNull String displayName() { return displayName; } @Override - public @NotNull String icon() { + public @NonNull String icon() { return icon; } @@ -103,6 +110,11 @@ public class GeyserCustomItemData implements CustomItemData { return renderOffsets; } + @Override + public @NonNull Set tags() { + return tags; + } + public static class CustomItemDataBuilder implements Builder { protected String name = null; protected CustomItemOptions customItemOptions = null; @@ -113,6 +125,7 @@ public class GeyserCustomItemData implements CustomItemData { protected boolean displayHandheld = false; protected int textureSize = 16; protected CustomRenderOffsets renderOffsets = null; + protected Set tags = new HashSet<>(); @Override public Builder name(@NonNull String name) { @@ -162,6 +175,12 @@ public class GeyserCustomItemData implements CustomItemData { return this; } + @Override + public Builder tags(@Nullable Set tags) { + this.tags = Objects.requireNonNullElseGet(tags, Set::of); + return this; + } + @Override public CustomItemData build() { if (this.name == null || this.customItemOptions == null) { @@ -174,7 +193,7 @@ public class GeyserCustomItemData implements CustomItemData { if (this.icon == null) { this.icon = this.name; } - return new GeyserCustomItemData(this.name, this.customItemOptions, this.displayName, this.icon, this.allowOffhand, this.displayHandheld, this.textureSize, this.renderOffsets); + return new GeyserCustomItemData(this.name, this.customItemOptions, this.displayName, this.icon, this.allowOffhand, this.displayHandheld, this.textureSize, this.renderOffsets, this.tags); } } } diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java index d6731e3b8..47b5aed33 100644 --- a/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java +++ b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java @@ -61,7 +61,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i public GeyserNonVanillaCustomItemData(NonVanillaCustomItemDataBuilder builder) { super(builder.name, builder.customItemOptions, builder.displayName, builder.icon, builder.allowOffhand, - builder.displayHandheld, builder.textureSize, builder.renderOffsets); + builder.displayHandheld, builder.textureSize, builder.renderOffsets, builder.tags); this.identifier = builder.identifier; this.javaId = builder.javaId; @@ -237,6 +237,11 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i return (NonVanillaCustomItemData.Builder) super.renderOffsets(renderOffsets); } + @Override + public NonVanillaCustomItemData.Builder tags(@Nullable Set tags) { + return (NonVanillaCustomItemData.Builder) super.tags(tags); + } + @Override public NonVanillaCustomItemData.Builder identifier(@NonNull String identifier) { this.identifier = identifier; diff --git a/core/src/main/java/org/geysermc/geyser/item/components/ToolBreakSpeedsUtils.java b/core/src/main/java/org/geysermc/geyser/item/components/ToolBreakSpeedsUtils.java deleted file mode 100644 index efefee946..000000000 --- a/core/src/main/java/org/geysermc/geyser/item/components/ToolBreakSpeedsUtils.java +++ /dev/null @@ -1,174 +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.item.components; - -import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.nbt.NbtType; - -import java.util.ArrayList; -import java.util.List; - -public class ToolBreakSpeedsUtils { - public static int toolTierToSpeed(String toolTier) { - ToolTier tier = ToolTier.getByName(toolTier); - if (tier != null) { - return tier.getSpeed(); - } - - return 0; - } - - private static NbtMap createTagBreakSpeed(int speed, String... tags) { - StringBuilder builder = new StringBuilder("query.any_tag('"); - builder.append(tags[0]); - for (int i = 1; i < tags.length; i++) { - builder.append("', '").append(tags[i]); - } - builder.append("')"); - - return NbtMap.builder() - .putCompound("block", NbtMap.builder() - .putString("tags", builder.toString()) - .build()) - .putCompound("on_dig", NbtMap.builder() - .putCompound("condition", NbtMap.builder() - .putString("expression", "") - .putInt("version", -1) - .build()) - .putString("event", "tool_durability") - .putString("target", "self") - .build()) - .putInt("speed", speed) - .build(); - } - - private static NbtMap createBreakSpeed(int speed, String block) { - return NbtMap.builder() - .putCompound("block", NbtMap.builder() - .putString("name", block).build()) - .putCompound("on_dig", NbtMap.builder() - .putCompound("condition", NbtMap.builder() - .putString("expression", "") - .putInt("version", -1) - .build()) - .putString("event", "tool_durability") - .putString("target", "self") - .build()) - .putInt("speed", speed) - .build(); - } - - private static NbtMap createDigger(List speeds) { - return NbtMap.builder() - .putList("destroy_speeds", NbtType.COMPOUND, speeds) - .putCompound("on_dig", NbtMap.builder() - .putCompound("condition", NbtMap.builder() - .putString("expression", "") - .putInt("version", -1) - .build()) - .putString("event", "tool_durability") - .putString("target", "self") - .build()) - .putBoolean("use_efficiency", true) - .build(); - } - - public static NbtMap getAxeDigger(int speed) { - List speeds = new ArrayList<>(); - speeds.add(createTagBreakSpeed(speed, "wood", "pumpkin", "plant")); - - return createDigger(speeds); - } - - public static NbtMap getPickaxeDigger(int speed, String toolTier) { - List speeds = new ArrayList<>(); - if (toolTier.equals(ToolTier.DIAMOND.toString()) || toolTier.equals(ToolTier.NETHERITE.toString())) { - speeds.add(createTagBreakSpeed(speed, "iron_pick_diggable", "diamond_pick_diggable")); - } else { - speeds.add(createTagBreakSpeed(speed, "iron_pick_diggable")); - } - speeds.add(createTagBreakSpeed(speed, "stone", "metal", "rail", "mob_spawner")); - - return createDigger(speeds); - } - - public static NbtMap getShovelDigger(int speed) { - List speeds = new ArrayList<>(); - speeds.add(createTagBreakSpeed(speed, "dirt", "sand", "gravel", "grass", "snow")); - - return createDigger(speeds); - } - - public static NbtMap getSwordDigger(int speed) { - List speeds = new ArrayList<>(); - speeds.add(createBreakSpeed(speed, "minecraft:web")); - speeds.add(createBreakSpeed(speed, "minecraft:bamboo")); - - return createDigger(speeds); - } - - public static NbtMap getHoeDigger(int speed) { - List speeds = new ArrayList<>(); - speeds.add(createBreakSpeed(speed, "minecraft:leaves")); - speeds.add(createBreakSpeed(speed, "minecraft:leaves2")); - speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves")); - speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves_flowered")); - - speeds.add(createBreakSpeed(speed, "minecraft:sculk")); - speeds.add(createBreakSpeed(speed, "minecraft:sculk_catalyst")); - speeds.add(createBreakSpeed(speed, "minecraft:sculk_sensor")); - speeds.add(createBreakSpeed(speed, "minecraft:sculk_shrieker")); - speeds.add(createBreakSpeed(speed, "minecraft:sculk_vein")); - - speeds.add(createBreakSpeed(speed, "minecraft:nether_wart_block")); - speeds.add(createBreakSpeed(speed, "minecraft:warped_wart_block")); - - speeds.add(createBreakSpeed(speed, "minecraft:hay_block")); - speeds.add(createBreakSpeed(speed, "minecraft:moss_block")); - speeds.add(createBreakSpeed(speed, "minecraft:shroomlight")); - speeds.add(createBreakSpeed(speed, "minecraft:sponge")); - speeds.add(createBreakSpeed(speed, "minecraft:target")); - - return createDigger(speeds); - } - - public static NbtMap getShearsDigger(int speed) { - List speeds = new ArrayList<>(); - speeds.add(createBreakSpeed(speed, "minecraft:web")); - - speeds.add(createBreakSpeed(speed, "minecraft:leaves")); - speeds.add(createBreakSpeed(speed, "minecraft:leaves2")); - speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves")); - speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves_flowered")); - - speeds.add(createBreakSpeed(speed, "minecraft:wool")); - - speeds.add(createBreakSpeed(speed, "minecraft:glow_lichen")); - speeds.add(createBreakSpeed(speed, "minecraft:vine")); - - return createDigger(speeds); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java index 97ab18c6b..11702e78a 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java @@ -62,6 +62,7 @@ public final class BlockStateValues { private static final IntSet ALL_PISTON_HEADS = new IntOpenHashSet(); private static final IntSet MOVING_PISTONS = new IntOpenHashSet(); private static final Int2ByteMap SKULL_VARIANTS = new FixedInt2ByteMap(); + private static final IntSet SKULL_POWERED = new IntOpenHashSet(); private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap(); private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap(); private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new FixedInt2ByteMap(); @@ -172,6 +173,13 @@ public final class BlockStateValues { SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue()); } + if (javaId.startsWith("minecraft:dragon_head[") || javaId.startsWith("minecraft:piglin_head[") + || javaId.startsWith("minecraft:dragon_wall_head[") || javaId.startsWith("minecraft:piglin_wall_head[")) { + if (javaId.contains("powered=true")) { + SKULL_POWERED.add(javaBlockState); + } + } + if (javaId.contains("wall_skull") || javaId.contains("wall_head")) { String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7, javaId.lastIndexOf("powered=") - 1); int rotation = switch (direction) { @@ -448,6 +456,17 @@ public final class BlockStateValues { return SKULL_ROTATIONS.getOrDefault(state, (byte) -1); } + /** + * As of Java 1.20.2: + * Skull powered states are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock. + * + * @param state BlockState of the block + * @return true if this skull is currently being powered. + */ + public static boolean isSkullPowered(int state) { + return SKULL_POWERED.contains(state); + } + /** * Skull rotations are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock. * This gives a integer rotation that Bedrock can use. diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java index e43e168ee..e401567e2 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java @@ -31,18 +31,19 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Value; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; 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.TransformationComponent; -import org.jetbrains.annotations.NotNull; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Objects; @Value public class GeyserCustomBlockComponents implements CustomBlockComponents { @@ -152,7 +153,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents { } @Override - public @NotNull Set tags() { + public @NonNull Set tags() { return tags; } @@ -170,7 +171,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents { protected TransformationComponent transformation; protected boolean unitCube = false; protected boolean placeAir = false; - protected final Set tags = new HashSet<>(); + protected Set tags = new HashSet<>(); private void validateBox(BoxComponent box) { if (box == null) { @@ -217,7 +218,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents { } @Override - public Builder materialInstance(@NotNull String name, @NotNull MaterialInstance materialInstance) { + public Builder materialInstance(@NonNull String name, @NonNull MaterialInstance materialInstance) { this.materialInstances.put(name, materialInstance); return this; } @@ -292,8 +293,8 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents { } @Override - public Builder tags(Set tags) { - this.tags.addAll(tags); + public Builder tags(@Nullable Set tags) { + this.tags = Objects.requireNonNullElseGet(tags, Set::of); return this; } 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 bf40ea2f9..5555375cd 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -93,6 +93,14 @@ public final class GameProtocol { return session.getUpstream().getProtocolVersion() < Bedrock_v594.CODEC.getProtocolVersion(); } + /** + * @param session the session to check + * @return true if the session needs an experiment for recipe unlocking + */ + public static boolean isUsingExperimentalRecipeUnlocking(GeyserSession session) { + return session.getUpstream().getProtocolVersion() == Bedrock_v594.CODEC.getProtocolVersion(); + } + /** * Gets the {@link PacketCodec} for Minecraft: Java Edition. * diff --git a/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java deleted file mode 100644 index 74e1430a2..000000000 --- a/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java +++ /dev/null @@ -1,303 +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.network; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.ping.GeyserPingInfo; -import org.geysermc.geyser.translator.text.MessageTranslator; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; - -public class QueryPacketHandler { - public static final byte HANDSHAKE = 0x09; - public static final byte STATISTICS = 0x00; - - private final GeyserImpl geyser; - private final InetSocketAddress sender; - private final byte type; - private final int sessionId; - private byte[] token; - - /** - * The Query packet handler instance. The unsigned short magic handshake should already be read at this point, - * and the packet should be verified to have enough buffer space to be a qualified query packet. - * - * @param geyser Geyser - * @param sender The Sender IP/Port for the Query - * @param buffer The Query data - */ - public QueryPacketHandler(GeyserImpl geyser, InetSocketAddress sender, ByteBuf buffer) { - this.geyser = geyser; - this.sender = sender; - this.type = buffer.readByte(); - this.sessionId = buffer.readInt(); - - regenerateToken(); - handle(); - } - - /** - * Checks the packet is in fact a query packet - * - * @param buffer Query data - * @return if the packet is a query packet - */ - public static boolean isQueryPacket(ByteBuf buffer) { - // 2 for magic short, 1 for type byte and 4 for session ID int - return buffer.readableBytes() >= (2 + 1 + 4) && buffer.readUnsignedShort() == 0xFEFD; - } - - /** - * Handles the query - */ - private void handle() { - switch (type) { - case HANDSHAKE: - sendToken(); - break; - case STATISTICS: - sendQueryData(); - break; - } - } - - /** - * Sends the token to the sender - */ - private void sendToken() { - ByteBuf reply = ByteBufAllocator.DEFAULT.ioBuffer(10); - reply.writeByte(HANDSHAKE); - reply.writeInt(sessionId); - reply.writeBytes(getTokenString(this.token, this.sender.getAddress())); - reply.writeByte(0); - - sendPacket(reply); - } - - /** - * Sends the query data to the sender - */ - private void sendQueryData() { - byte[] gameData = getGameData(); - byte[] playerData = getPlayers(); - - ByteBuf reply = ByteBufAllocator.DEFAULT.ioBuffer(1 + 4 + gameData.length + playerData.length); - reply.writeByte(STATISTICS); - reply.writeInt(sessionId); - - // Game Info - reply.writeBytes(gameData); - - // Players - reply.writeBytes(playerData); - - sendPacket(reply); - } - - /** - * Gets the game data for the query - * - * @return the game data for the query - */ - private byte[] getGameData() { - ByteArrayOutputStream query = new ByteArrayOutputStream(); - - GeyserPingInfo pingInfo = null; - String motd; - String currentPlayerCount; - String maxPlayerCount; - String map; - - if (geyser.getConfig().isPassthroughMotd() || geyser.getConfig().isPassthroughPlayerCounts()) { - pingInfo = geyser.getBootstrap().getGeyserPingPassthrough().getPingInformation(); - } - - if (geyser.getConfig().isPassthroughMotd() && pingInfo != null) { - String[] javaMotd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); - motd = javaMotd[0].trim(); // First line of the motd. - } else { - motd = geyser.getConfig().getBedrock().primaryMotd(); - } - - // If passthrough player counts is enabled lets get players from the server - if (geyser.getConfig().isPassthroughPlayerCounts() && pingInfo != null) { - currentPlayerCount = String.valueOf(pingInfo.getPlayers().getOnline()); - maxPlayerCount = String.valueOf(pingInfo.getPlayers().getMax()); - } else { - currentPlayerCount = String.valueOf(geyser.getSessionManager().getSessions().size()); - maxPlayerCount = String.valueOf(geyser.getConfig().getMaxPlayers()); - } - - // If passthrough protocol name is enabled let's get the protocol name from the ping response. - if (geyser.getConfig().isPassthroughProtocolName() && pingInfo != null) { - map = pingInfo.getVersion().getName(); - } else { - map = GeyserImpl.NAME; - } - - // Create a hashmap of all game data needed in the query - Map gameData = new HashMap<>(); - gameData.put("hostname", motd); - gameData.put("gametype", "SMP"); - gameData.put("game_id", "MINECRAFT"); - gameData.put("version", GeyserImpl.NAME + " (" + GeyserImpl.GIT_VERSION + ") " + GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()); - gameData.put("plugins", ""); - gameData.put("map", map); - gameData.put("numplayers", currentPlayerCount); - gameData.put("maxplayers", maxPlayerCount); - gameData.put("hostport", String.valueOf(geyser.getConfig().getBedrock().port())); - gameData.put("hostip", geyser.getConfig().getBedrock().address()); - - try { - writeString(query, "GeyserMC"); - query.write((byte) 0x80); - query.write((byte) 0x00); - - // Fills the game data - for (Map.Entry entry : gameData.entrySet()) { - writeString(query, entry.getKey()); - writeString(query, entry.getValue()); - } - - // Final byte to show the end of the game data - query.write(new byte[] { 0x00, 0x01 }); - return query.toByteArray(); - } catch (IOException e) { - e.printStackTrace(); - return new byte[0]; - } - } - - /** - * Generate a byte[] storing the player names - * - * @return The byte[] representation of players - */ - private byte[] getPlayers() { - ByteArrayOutputStream query = new ByteArrayOutputStream(); - - GeyserPingInfo pingInfo = null; - if (geyser.getConfig().isPassthroughMotd() || geyser.getConfig().isPassthroughPlayerCounts()) { - pingInfo = geyser.getBootstrap().getGeyserPingPassthrough().getPingInformation(); - } - - try { - // Start the player section - writeString(query, "player_"); - query.write((byte) 0x00); - - // Fill player names - if (pingInfo != null) { - for (String username : pingInfo.getPlayerList()) { - writeString(query, username); - } - } - - // Final byte to show the end of the player data - query.write((byte) 0x00); - return query.toByteArray(); - } catch (IOException e) { - e.printStackTrace(); - return new byte[0]; - } - } - - /** - * Partially mimics {@link java.io.DataOutputStream#writeBytes(String)} which is what the Minecraft server uses as of 1.17.1. - */ - private void writeString(OutputStream stream, String value) throws IOException { - int length = value.length(); - for (int i = 0; i < length; i++) { - stream.write((byte) value.charAt(i)); - } - // Padding to indicate the end of the string - stream.write((byte) 0x00); - } - - /** - * Sends a packet to the sender - * - * @param data packet data - */ - private void sendPacket(ByteBuf data) { - // geyser.getBedrockServer().getRakNet().send(sender, data); - } - - /** - * Regenerates a token - */ - public void regenerateToken() { - byte[] token = new byte[16]; - for (int i = 0; i < 16; i++) { - token[i] = (byte) ThreadLocalRandom.current().nextInt(255); - } - - this.token = token; - } - - /** - * Gets an MD5 token for the current IP/Port. - * This should reset every 30 seconds but a new one is generated per instance - * Seems wasteful to code something in to clear it when it has no use. - * - * @param token the token - * @param address the address - * @return an MD5 token for the current IP/Port - */ - public static byte[] getTokenString(byte[] token, InetAddress address) { - try { - // Generate an MD5 hash from the address - MessageDigest digest = MessageDigest.getInstance("MD5"); - digest.update(address.toString().getBytes(StandardCharsets.UTF_8)); - digest.update(token); - - // Get the first 4 bytes of the digest - byte[] digestBytes = Arrays.copyOf(digest.digest(), 4); - - // Convert the bytes to a buffer - ByteBuffer byteBuffer = ByteBuffer.wrap(digestBytes); - - // Turn the number into a null terminated string - return (byteBuffer.getInt() + "\0").getBytes(); - } catch (NoSuchAlgorithmException e) { - return (ByteBuffer.allocate(4).putInt(ThreadLocalRandom.current().nextInt()).getInt() + "\0").getBytes(); - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java index c89da0bdd..20340826f 100644 --- a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java +++ b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java @@ -69,6 +69,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.IntFunction; +import java.util.function.Supplier; public final class GeyserServer { private static final boolean PRINT_DEBUG_PINGS = Boolean.parseBoolean(System.getProperty("Geyser.PrintPingsInDebugMode", "true")); @@ -163,8 +164,9 @@ public final class GeyserServer { if (System.getProperties().contains("disableNativeEventLoop")) { this.geyser.getLogger().debug("EventLoop type is NIO because native event loops are disabled."); } else { - this.geyser.getLogger().debug("Reason for no Epoll: " + Epoll.unavailabilityCause().toString()); - this.geyser.getLogger().debug("Reason for no KQueue: " + KQueue.unavailabilityCause().toString()); + // Use lambda here, not method reference, or else NoClassDefFoundError for Epoll/KQueue will not be caught + this.geyser.getLogger().debug("Reason for no Epoll: " + throwableOrCaught(() -> Epoll.unavailabilityCause())); + this.geyser.getLogger().debug("Reason for no KQueue: " + throwableOrCaught(() -> KQueue.unavailabilityCause())); } } } @@ -230,7 +232,9 @@ public final class GeyserServer { GeyserPingInfo pingInfo = null; if (config.isPassthroughMotd() || config.isPassthroughPlayerCounts()) { IGeyserPingPassthrough pingPassthrough = geyser.getBootstrap().getGeyserPingPassthrough(); - pingInfo = pingPassthrough.getPingInformation(inetSocketAddress); + if (pingPassthrough != null) { + pingInfo = pingPassthrough.getPingInformation(inetSocketAddress); + } } BedrockPong pong = new BedrockPong() @@ -245,8 +249,8 @@ public final class GeyserServer { if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); - String mainMotd = motd[0]; // First line of the motd. - String subMotd = (motd.length != 1) ? motd[1] : GeyserImpl.NAME; // Second line of the motd if present, otherwise default. + 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. 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. @@ -312,6 +316,17 @@ public final class GeyserServer { return pong; } + /** + * @return the throwable from the given supplier, or the throwable caught while calling the supplier. + */ + private static Throwable throwableOrCaught(Supplier supplier) { + try { + return supplier.get(); + } catch (Throwable throwable) { + return throwable; + } + } + private static Transport compatibleTransport() { TransportHelper.TransportMethod transportMethod = TransportHelper.determineTransportMethod(); if (transportMethod == TransportHelper.TransportMethod.EPOLL) { 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 d444e554c..9d8da114d 100644 --- a/core/src/main/java/org/geysermc/geyser/ping/GeyserPingInfo.java +++ b/core/src/main/java/org/geysermc/geyser/ping/GeyserPingInfo.java @@ -25,34 +25,37 @@ package org.geysermc.geyser.ping; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; -import java.util.ArrayList; -import java.util.Collection; - +/** + * 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 private String description; private Players players; - private Version version; - - @JsonIgnore - private Collection playerList = new ArrayList<>(); public GeyserPingInfo() { + // for json mapping } - public GeyserPingInfo(String description, Players players, Version version) { + public GeyserPingInfo(@Nullable String description, Players players) { this.description = description; this.players = players; - this.version = version; + } + + public GeyserPingInfo(@Nullable String description, int maxPlayers, int onlinePlayers) { + this.description = description; + this.players = new Players(maxPlayers, onlinePlayers); } @JsonSetter("description") @@ -68,6 +71,7 @@ public class GeyserPingInfo { private int online; public Players() { + // for json mapping } public Players(int max, int online) { @@ -75,19 +79,4 @@ public class GeyserPingInfo { this.online = online; } } - - @Data - public static class Version { - - private String name; - private int protocol; - - public Version() { - } - - public Version(String name, int protocol) { - this.name = name; - this.protocol = protocol; - } - } } diff --git a/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java b/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java index d414b7fa8..b6095cce2 100644 --- a/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java +++ b/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java @@ -26,7 +26,6 @@ package org.geysermc.geyser.ping; import javax.annotation.Nullable; -import java.net.Inet4Address; import java.net.InetSocketAddress; /** @@ -34,16 +33,6 @@ import java.net.InetSocketAddress; */ public interface IGeyserPingPassthrough { - /** - * Get the MOTD of the server displayed on the multiplayer screen. It uses a fake remote, as the remote isn't important in this context. - * - * @return string of the MOTD - */ - @Nullable - default GeyserPingInfo getPingInformation() { - return this.getPingInformation(new InetSocketAddress(Inet4Address.getLoopbackAddress(), 69)); - } - /** * Get the MOTD of the server displayed on the multiplayer screen * 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 e63bf6bf9..fb4a6acbe 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 @@ -198,6 +198,12 @@ public class MappingsReader_v1 extends MappingsReader { customItemData.renderOffsets(fromJsonNode(tmpNode)); } + if (node.get("tags") instanceof ArrayNode tags) { + Set tagsSet = new ObjectOpenHashSet<>(); + tags.forEach(tag -> tagsSet.add(tag.asText())); + customItemData.tags(tagsSet); + } + return customItemData.build(); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java index 3f3f5a4ba..638e41a5e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java @@ -270,6 +270,17 @@ public class CustomItemRegistryPopulator { .build()); componentBuilder.putCompound("minecraft:display_name", NbtMap.builder().putString("value", customItemData.displayName()).build()); + // Add a Geyser tag to the item, allowing Molang queries + addItemTag(componentBuilder, "geyser:is_custom"); + + // Add other defined tags to the item + Set tags = customItemData.tags(); + for (String tag : tags) { + if (tag != null && !tag.isBlank()) { + addItemTag(componentBuilder, tag); + } + } + itemProperties.putBoolean("allow_off_hand", customItemData.allowOffhand()); itemProperties.putBoolean("hand_equipped", displayHandheld); itemProperties.putInt("max_stack_size", stackSize); @@ -313,7 +324,7 @@ public class CustomItemRegistryPopulator { .build() )); - componentBuilder.putCompound("minecraft:digger", + componentBuilder.putCompound("minecraft:digger", NbtMap.builder() .putList("destroy_speeds", NbtType.COMPOUND, speed) .putCompound("on_dig", NbtMap.builder() @@ -506,8 +517,19 @@ public class CustomItemRegistryPopulator { return List.of(xyz.x(), xyz.y(), xyz.z()); } - private static void setItemTag(NbtMapBuilder builder, String tag) { - builder.putList("item_tags", NbtType.STRING, List.of("minecraft:is_" + tag)); + @SuppressWarnings("unchecked") + private static void addItemTag(NbtMapBuilder builder, String tag) { + List tagList = (List) builder.get("item_tags"); + if (tagList == null) { + builder.putList("item_tags", NbtType.STRING, tag); + } else { + // NbtList is immutable + if (!tagList.contains(tag)) { + tagList = new ArrayList<>(tagList); + tagList.add(tag); + builder.putList("item_tags", NbtType.STRING, tagList); + } + } } private static NbtMap xyzToScaleList(float x, float y, float z) { 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 35b532905..ebb0025da 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -105,6 +105,7 @@ import org.geysermc.geyser.api.bedrock.camera.CameraShake; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; +import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent; import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.RemoteServer; @@ -128,6 +129,7 @@ import org.geysermc.geyser.item.Items; import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.netty.LocalSession; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.BlockMappings; @@ -391,6 +393,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private Entity mouseoverEntity; + /** + * Stores all Java recipes by recipe identifier, and matches them to all possible Bedrock recipe identifiers. + * They are not 1:1, since Bedrock can have multiple recipes for the same Java recipe. + */ + @Setter + private Map> javaToBedrockRecipeIds; + @Setter private Int2ObjectMap craftingRecipes; private final AtomicInteger lastRecipeNetId; @@ -611,6 +620,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { this.playerInventory = new PlayerInventory(); this.openInventory = null; this.craftingRecipes = new Int2ObjectOpenHashMap<>(); + this.javaToBedrockRecipeIds = new Object2ObjectOpenHashMap<>(); this.lastRecipeNetId = new AtomicInteger(1); this.spawned = false; @@ -690,6 +700,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { gamerulePacket.getGameRules().add(new GameRuleData<>("keepinventory", true)); // Ensure client doesn't try and do anything funky; the server handles this for us gamerulePacket.getGameRules().add(new GameRuleData<>("spawnradius", 0)); + // Recipe unlocking - only needs to be added if 1. it isn't already on via an experiment, or 2. the client is on pre 1.20.10 + if (!GameProtocol.isPre1_20_10(this) && !GameProtocol.isUsingExperimentalRecipeUnlocking(this)) { + gamerulePacket.getGameRules().add(new GameRuleData<>("recipesunlock", true)); + } upstream.sendPacket(gamerulePacket); } @@ -1025,10 +1039,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect", authData.name(), remoteServer.address(), disconnectMessage)); } if (cause != null) { - cause.printStackTrace(); + GeyserImpl.getInstance().getLogger().error(cause.getMessage()); + // GeyserSession is disconnected via session.disconnect() called indirectly be the server + // This only needs to be "initiated" here when there is an exception, hence the cause clause + GeyserSession.this.disconnect(disconnectMessage); + if (geyser.getConfig().isDebugMode()) { + cause.printStackTrace(); + } } - - upstream.disconnect(disconnectMessage); } @Override @@ -1055,17 +1073,30 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { public void disconnect(String reason) { if (!closed) { loggedIn = false; + + // Fire SessionDisconnectEvent + SessionDisconnectEvent disconnectEvent = new SessionDisconnectEvent(this, reason); + geyser.getEventBus().fire(disconnectEvent); + + // Disconnect downstream if necessary if (downstream != null) { - downstream.disconnect(reason); + // No need to disconnect if already closed + if (!downstream.isClosed()) { + downstream.disconnect(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() : ""; geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.disconnect", address, reason)); } + + // Disconnect upstream if necessary if (!upstream.isClosed()) { - upstream.disconnect(reason); + upstream.disconnect(disconnectEvent.disconnectReason()); } + + // Remove from session manager geyser.getSessionManager().removeSession(this); if (authData != null) { PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid()); @@ -1527,6 +1558,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { startGamePacket.setRewindHistorySize(0); startGamePacket.setServerAuthoritativeBlockBreaking(false); + if (GameProtocol.isUsingExperimentalRecipeUnlocking(this)) { + startGamePacket.getExperiments().add(new ExperimentData("recipe_unlocking", true)); + } + upstream.sendPacket(startGamePacket); } 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 ef462a3e3..35ede56a1 100644 --- a/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/UpstreamSession.java @@ -58,7 +58,7 @@ public class UpstreamSession { } public void disconnect(String reason) { - session.disconnect(reason); + this.session.disconnect(reason); } /** diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java index 9dc89215a..8a4b9cb6c 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java @@ -162,4 +162,9 @@ public class EntityCache { public List getTickableEntities() { return tickableEntities; } + + public void removeAllBossBars() { + bossBars.values().forEach(BossBar::removeBossBar); + bossBars.clear(); + } } 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 76cdbb5a4..7f87f9465 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 @@ -60,6 +60,9 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements } builder.put("Rotation", rotation); builder.put("SkullType", skullVariant); + if (BlockStateValues.isSkullPowered(blockState)) { + builder.putBoolean("MouthMoving", true); + } } private static UUID getUUID(CompoundTag owner) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaClientboundRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaClientboundRecipesTranslator.java new file mode 100644 index 000000000..1ccbcfdec --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaClientboundRecipesTranslator.java @@ -0,0 +1,79 @@ +/* + * 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.translator.protocol.java; + +import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundRecipePacket; +import org.cloudburstmc.protocol.bedrock.packet.UnlockedRecipesPacket; +import org.geysermc.geyser.network.GameProtocol; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; + +import java.util.ArrayList; +import java.util.List; + +@Translator(packet = ClientboundRecipePacket.class) +public class JavaClientboundRecipesTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundRecipePacket packet) { + // recipe unlocking does not exist pre 1.20.10 + if (GameProtocol.isPre1_20_10(session)) { + return; + } + + UnlockedRecipesPacket recipesPacket = new UnlockedRecipesPacket(); + switch (packet.getAction()) { + case INIT -> { + recipesPacket.setAction(UnlockedRecipesPacket.ActionType.INITIALLY_UNLOCKED); + recipesPacket.getUnlockedRecipes().addAll(getBedrockRecipes(session, packet.getAlreadyKnownRecipes())); + } + case ADD -> { + recipesPacket.setAction(UnlockedRecipesPacket.ActionType.NEWLY_UNLOCKED); + recipesPacket.getUnlockedRecipes().addAll(getBedrockRecipes(session, packet.getRecipes())); + } + case REMOVE -> { + recipesPacket.setAction(UnlockedRecipesPacket.ActionType.REMOVE_UNLOCKED); + recipesPacket.getUnlockedRecipes().addAll(getBedrockRecipes(session, packet.getRecipes())); + } + } + session.sendUpstreamPacket(recipesPacket); + } + + private List getBedrockRecipes(GeyserSession session, String[] javaRecipeIdentifiers) { + List recipes = new ArrayList<>(); + for (String javaIdentifier : javaRecipeIdentifiers) { + List bedrockRecipes = session.getJavaToBedrockRecipeIds().get(javaIdentifier); + // Some recipes are not (un)lockable on Bedrock edition, like furnace or stonecutter recipes. + // So we don't store/send these. + if (bedrockRecipes != null) { + recipes.addAll(bedrockRecipes); + } + } + return recipes; + } +} + 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 3b9f29b89..377f1d2cf 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 @@ -28,8 +28,11 @@ package org.geysermc.geyser.translator.protocol.java; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerSpawnInfo; import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket; +import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.GameRuleData; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket; import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.geyser.api.network.AuthType; @@ -64,12 +67,36 @@ public class JavaLoginTranslator extends PacketTranslator RECIPE_TAGS = Map.of( + "minecraft:wood", "minecraft:logs", + "minecraft:wooden_slab", "minecraft:wooden_slabs", + "minecraft:planks", "minecraft:planks"); + @Override public void translate(GeyserSession session, ClientboundUpdateRecipesPacket packet) { Map> recipeTypes = Registries.CRAFTING_DATA.forVersion(session.getUpstream().getProtocolVersion()); // Get the last known network ID (first used for the pregenerated recipes) and increment from there. int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1; boolean sendTrimRecipes = false; - + Map> recipeIDs = session.getJavaToBedrockRecipeIds(); Int2ObjectMap recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion())); Int2ObjectMap> unsortedStonecutterData = new Int2ObjectOpenHashMap<>(); CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); craftingDataPacket.setCleanRecipes(true); + for (Recipe recipe : packet.getRecipes()) { switch (recipe.getType()) { case CRAFTING_SHAPELESS -> { @@ -121,12 +131,15 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator bedrockRecipeIDs = new ArrayList<>(); for (ItemDescriptorWithCount[] inputs : inputCombinations) { UUID uuid = UUID.randomUUID(); + bedrockRecipeIDs.add(uuid.toString()); craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(), Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId)); recipeMap.put(netId++, new GeyserShapelessRecipe(shapelessRecipeData)); } + addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs); } case CRAFTING_SHAPED -> { ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData(); @@ -141,13 +154,17 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator bedrockRecipeIDs = new ArrayList<>(); for (ItemDescriptorWithCount[] inputs : inputCombinations) { UUID uuid = UUID.randomUUID(); + bedrockRecipeIDs.add(uuid.toString()); craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData.shaped(uuid.toString(), shapedRecipeData.getWidth(), shapedRecipeData.getHeight(), Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId)); recipeMap.put(netId++, new GeyserShapedRecipe(shapedRecipeData)); } + addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs); } case STONECUTTING -> { StoneCuttingRecipeData stoneCuttingData = (StoneCuttingRecipeData) recipe.getData(); @@ -157,8 +174,8 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator(); unsortedStonecutterData.put(ingredient.getId(), data); } - data.add(stoneCuttingData); // Save for processing after all recipes have been received + data.add(stoneCuttingData); } case SMITHING_TRANSFORM -> { SmithingTransformRecipeData data = (SmithingTransformRecipeData) recipe.getData(); @@ -173,21 +190,29 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator(Collections.singletonList(id))); } } } - } case SMITHING_TRIM -> { sendTrimRecipes = true; // ignored currently - see below } + case CRAFTING_DECORATED_POT -> { + // Paper 1.20 seems to send only one recipe, which seems to be hardcoded to include all recipes. + // We can send the equivalent Bedrock MultiRecipe! :) + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("685a742a-c42e-4a4e-88ea-5eb83fc98e5b"), netId++)); + } default -> { List craftingData = recipeTypes.get(recipe.getType()); if (craftingData != null) { + addSpecialRecipesIdentifiers(session, recipe, craftingData); craftingDataPacket.getCraftingData().addAll(craftingData); } } @@ -218,14 +243,15 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator craftingData) { + String javaRecipeID = recipe.getIdentifier(); + + switch (recipe.getType()) { + case CRAFTING_SPECIAL_BOOKCLONING, CRAFTING_SPECIAL_REPAIRITEM, CRAFTING_SPECIAL_MAPEXTENDING, CRAFTING_SPECIAL_MAPCLONING: + // We do not want to (un)lock these, since BDS does not do it for MultiRecipes + return; + case CRAFTING_SPECIAL_SHULKERBOXCOLORING: + // BDS (un)locks the dyeing with the shulker box recipe, Java never - we want BDS behavior for ease of use + javaRecipeID = "minecraft:shulker_box"; + break; + case CRAFTING_SPECIAL_TIPPEDARROW: + // similar as above + javaRecipeID = "minecraft:arrow"; + break; + } + List bedrockRecipeIDs = new ArrayList<>(); + + // defined in the recipes.json mappings file: Only tipped arrows use shaped recipes, we need the cast for the identifier + if (recipe.getType() == RecipeType.CRAFTING_SPECIAL_TIPPEDARROW) { + for (RecipeData data : craftingData) { + bedrockRecipeIDs.add(((org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData) data).getId()); + } + } else { + for (RecipeData data : craftingData) { + bedrockRecipeIDs.add(((org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData) data).getId()); + } + } + addRecipeIdentifier(session, javaRecipeID, bedrockRecipeIDs); } //TODO: rewrite @@ -277,6 +335,13 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator> entry : groupedByIds.entrySet()) { if (entry.getValue().size() > 1) { GroupedItem groupedItem = entry.getKey(); + + String recipeTag = RECIPE_TAGS.get(groupedItem.id.getIdentifier()); + if (recipeTag != null) { + optionSet.add(new ItemDescriptorWithCount(new ItemTagDescriptor(recipeTag), groupedItem.count)); + continue; + } + int idCount = 0; //not optimal for (ItemMapping mapping : session.getItemMappings().getItems()) { @@ -337,6 +402,10 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator bedrockIdentifiers) { + session.getJavaToBedrockRecipeIds().computeIfAbsent(javaIdentifier, k -> new ArrayList<>()).addAll(bedrockIdentifiers); + } + @EqualsAndHashCode @AllArgsConstructor private static class GroupedItem { diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 8e4db5e38..218e13833 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -91,9 +91,6 @@ 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: false -# Relay the protocol name (e.g. BungeeCord [X.X], Paper 1.X) - only really useful when using a custom protocol name! -# This will also show up on sites like MCSrvStatus. -passthrough-protocol-name: false # Relay the player count and max players from the remote server to Bedrock players. passthrough-player-counts: false # Enable LEGACY ping passthrough. There is no need to enable this unless your MOTD or player count does not appear properly.