diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 862e9c0be..7bd9a19d0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,10 +1,10 @@ blank_issues_enabled: false contact_links: - name: Common Issues - url: https://github.com/GeyserMC/Geyser/wiki/Common-Issues + url: https://wiki.geysermc.org/geyser/common-issues about: Check the common issues to see if you are not alone with that issue and see how you can fix them. - name: Frequently Asked Questions - url: https://github.com/GeyserMC/Geyser/wiki/FAQ + url: https://wiki.geysermc.org/geyser/faq about: Look at the FAQ page for answers to frequently asked questions. - name: Get help on the GeyserMC Discord server url: https://discord.gg/geysermc diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index f5bb4c042..9d925c4dc 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -9,11 +9,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 16 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: distribution: 'temurin' - java-version: 16 + java-version: 17 cache: 'gradle' - name: submodules-init uses: snickerbockers/submodules-init@v4 diff --git a/Jenkinsfile b/Jenkinsfile index f92778318..b3df4bc95 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,7 +2,7 @@ pipeline { agent any tools { gradle 'Gradle 7' - jdk 'Java 16' + jdk 'Java 17' } options { buildDiscarder(logRotator(artifactNumToKeepStr: '20')) diff --git a/README.md b/README.md index aba8babf2..796170dfd 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here! -### Currently supporting Minecraft Bedrock 1.19 and Minecraft Java 1.19.0. +### Currently supporting Minecraft Bedrock 1.19.0 - 1.19.10 and Minecraft Java 1.19.0. ## Setting Up Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index 8e2b73cd1..02883999d 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -1,6 +1,6 @@ -val paperVersion = "1.17.1-R0.1-SNAPSHOT" // Needed because we do not support Java 17 yet +val paperVersion = "1.19-R0.1-SNAPSHOT" val viaVersion = "4.0.0" -val adaptersVersion = "1.4-SNAPSHOT" +val adaptersVersion = "1.5-SNAPSHOT" val commodoreVersion = "1.13" dependencies { @@ -9,6 +9,18 @@ dependencies { implementation("org.geysermc.geyser.adapters", "spigot-all", adaptersVersion) implementation("me.lucko", "commodore", commodoreVersion) + + // Both paper-api and paper-mojangapi only provide Java 17 versions for 1.19 + compileOnly("io.papermc.paper", "paper-api", paperVersion) { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17) + } + } + compileOnly("io.papermc.paper", "paper-mojangapi", paperVersion) { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17) + } + } } platformRelocate("it.unimi.dsi.fastutil") @@ -19,8 +31,6 @@ platformRelocate("me.lucko.commodore") platformRelocate("io.netty.channel.kqueue") // These dependencies are already present on the platform -provided("io.papermc.paper", "paper-api", paperVersion) -provided("io.papermc.paper", "paper-mojangapi", paperVersion) provided("com.viaversion", "viaversion", viaVersion) application { 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 8d0641599..36dd81d44 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 @@ -35,6 +35,7 @@ import org.geysermc.geyser.ping.IGeyserPingPassthrough; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Constructor; import java.net.InetSocketAddress; /** @@ -42,6 +43,8 @@ import java.net.InetSocketAddress; * applied. */ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough { + private static final Constructor<PaperServerListPingEvent> OLD_CONSTRUCTOR = ReflectedNames.getOldPaperPingConstructor(); + private final GeyserSpigotLogger logger; public GeyserPaperPingPassthrough(GeyserSpigotLogger logger) { @@ -54,9 +57,17 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough 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); + PaperServerListPingEvent event; + if (OLD_CONSTRUCTOR != null) { + // Approximately pre-1.19 + event = OLD_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress), + Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), + Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null); + } else { + event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress), + Bukkit.getMotd(), Bukkit.shouldSendChatPreviews(), 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. @@ -80,7 +91,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough } return geyserPingInfo; - } catch (Exception e) { + } 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 eb328735d..634d1f8a8 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 @@ -56,7 +56,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough { ); Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add); return geyserPingInfo; - } catch (Exception e) { + } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change logger.debug("Error while getting Bukkit ping passthrough: " + e); return null; } @@ -66,7 +66,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough { private static class GeyserPingEvent extends ServerListPingEvent { public GeyserPingEvent(InetAddress address, String motd, int numPlayers, int maxPlayers) { - super(address, motd, numPlayers, maxPlayers); + super(address, motd, Bukkit.shouldSendChatPreviews(), numPlayers, maxPlayers); } @Override 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 fed5dd6b9..20f4305dd 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,14 +168,16 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { if (geyserConfig.isLegacyPingPassthrough()) { this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } else { - try { - Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent"); + if (ReflectedNames.checkPaperPingEvent()) { this.geyserSpigotPingPassthrough = new GeyserPaperPingPassthrough(geyserLogger); - } catch (ClassNotFoundException e) { + } else if (ReflectedNames.newSpigotPingConstructorExists()) { this.geyserSpigotPingPassthrough = new GeyserSpigotPingPassthrough(geyserLogger); + } else { + // Can't enable one of the other options + this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } } - geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass())); + geyserLogger.info("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass())); this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); this.geyserCommandManager.init(); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java new file mode 100644 index 000000000..3185f2d30 --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java @@ -0,0 +1,90 @@ +/* + * 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 org.bukkit.event.server.ServerListPingEvent; +import org.bukkit.util.CachedServerIcon; + +import javax.annotation.Nullable; +import java.lang.reflect.Constructor; +import java.net.InetAddress; + +/** + * A utility class for checking on the existence of classes, constructors, fields, methods + */ +public final class ReflectedNames { + + static boolean checkPaperPingEvent() { + return classExists("com.destroystokyo.paper.event.server.PaperServerListPingEvent"); + } + + /** + * @return if this class name exists + */ + private static boolean classExists(String clazz) { + try { + Class.forName(clazz); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + static boolean newSpigotPingConstructorExists() { + return getConstructor(ServerListPingEvent.class, InetAddress.class, String.class, boolean.class, int.class, int.class) != null; + } + + static Constructor<PaperServerListPingEvent> getOldPaperPingConstructor() { + if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, boolean.class, int.class, + int.class, String.class, int.class, CachedServerIcon.class) != null) { + // @NotNull StatusClient client, @NotNull String motd, boolean shouldSendChatPreviews, int numPlayers, int maxPlayers, + // @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon + // New constructor is present + return null; + } + // @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers, + // @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon + return getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class, int.class, + String.class, int.class, CachedServerIcon.class); + } + + /** + * @return if this class has a constructor with the specified arguments + */ + @Nullable + private static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... args) { + try { + return clazz.getConstructor(args); + } catch (NoSuchMethodException e) { + return null; + } + } + + private ReflectedNames() { + } +} diff --git a/build-logic/src/main/kotlin/Versions.kt b/build-logic/src/main/kotlin/Versions.kt index 779065bc5..27f7bcaf5 100644 --- a/build-logic/src/main/kotlin/Versions.kt +++ b/build-logic/src/main/kotlin/Versions.kt @@ -30,10 +30,12 @@ object Versions { const val guavaVersion = "29.0-jre" const val nbtVersion = "2.1.0" const val websocketVersion = "1.5.1" - const val protocolVersion = "977a9a1" + const val protocolVersion = "a78a64b" + // Not pinned to specific version due to possible gradle bug + // See comment in settings.gradle.kts const val raknetVersion = "1.6.28-SNAPSHOT" const val mcauthlibVersion = "d9d773e" - const val mcprotocollibversion = "bb2b414" + const val mcprotocollibversion = "54fc9f0" const val packetlibVersion = "3.0" const val adventureVersion = "4.9.3" const val eventVersion = "3.0.0" diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e82af5687..561c2f554 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { // Network libraries implementation("org.java-websocket", "Java-WebSocket", Versions.websocketVersion) - api("com.github.CloudburstMC.Protocol", "bedrock-v527", Versions.protocolVersion) { + api("com.github.CloudburstMC.Protocol", "bedrock-v534", Versions.protocolVersion) { exclude("com.nukkitx.network", "raknet") exclude("com.nukkitx", "nbt") } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java index e60daacd3..bba2e8d21 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java @@ -47,7 +47,7 @@ public class OffhandCommand extends GeyserCommand { } ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO, - Direction.DOWN, session.getNextSequence()); + Direction.DOWN, session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(releaseItemPacket); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java index 75bdd9021..65662bbe4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java @@ -99,19 +99,9 @@ public class FishingHookEntity extends ThrowableEntity { } } - int waterLevel = BlockStateValues.getWaterLevel(blockID); - if (BlockRegistries.WATERLOGGED.get().contains(blockID)) { - waterLevel = 0; - } - if (waterLevel >= 0) { - double waterMaxY = iter.getY() + 1 - (waterLevel + 1) / 9.0; - // Falling water is a full block - if (waterLevel >= 8) { - waterMaxY = iter.getY() + 1; - } - if (position.getY() <= waterMaxY) { - touchingWater = true; - } + double waterHeight = BlockStateValues.getWaterHeight(blockID); + if (waterHeight != -1 && position.getY() <= (iter.getY() + waterHeight)) { + touchingWater = true; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java index 04e4727d0..75b2ad991 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java @@ -51,6 +51,7 @@ public class ArmorStandEntity extends LivingEntity { @Getter private boolean isMarker = false; private boolean isInvisible = false; + @Getter private boolean isSmall = false; /** @@ -74,6 +75,7 @@ public class ArmorStandEntity extends LivingEntity { * - No armor, no name: false * - No armor, yes name: true */ + @Getter private boolean positionRequiresOffset = false; /** * Whether we should update the position of this armor stand after metadata updates. @@ -411,6 +413,8 @@ public class ArmorStandEntity extends LivingEntity { this.positionRequiresOffset = newValue; if (positionRequiresOffset) { this.position = applyOffsetToPosition(position); + // Update the passenger offset as armorstand is moving up by roughly 2 blocks + updatePassengerOffsets(); } else { this.position = removeOffsetFromPosition(position); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java index e5cbb2f89..52e4a6f2f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java @@ -45,6 +45,9 @@ public class IronGolemEntity extends GolemEntity { setFlag(EntityFlag.BRIBED, true); // Required, or else the overlay is black dirtyMetadata.put(EntityData.COLOR_2, (byte) 0); + // Default max health. Ensures correct cracked texture is used + // Bug reproducible in 1.19.0 JE vanilla/fabric when spawning a new iron golem + maxHealth = 100f; } @Nonnull diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java index 1ca34037c..ff6eed975 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java @@ -41,7 +41,7 @@ import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; public class WardenEntity extends MonsterEntity implements Tickable { - private int heartBeatDelay; + private int heartBeatDelay = 40; private int tickCount; private int sonicBoomTickDuration; @@ -50,6 +50,12 @@ public class WardenEntity extends MonsterEntity implements Tickable { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + dirtyMetadata.put(EntityData.HEARTBEAT_INTERVAL_TICKS, heartBeatDelay); + } + @Override public void setPose(Pose pose) { setFlag(EntityFlag.DIGGING, pose == Pose.DIGGING); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index 6f2958ffd..8e600b1a8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -35,9 +35,7 @@ import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; -import com.nukkitx.protocol.bedrock.data.AttributeData; -import com.nukkitx.protocol.bedrock.data.GameType; -import com.nukkitx.protocol.bedrock.data.PlayerPermission; +import com.nukkitx.protocol.bedrock.data.*; import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; @@ -59,6 +57,7 @@ import org.geysermc.geyser.translator.text.MessageTranslator; import javax.annotation.Nullable; import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -66,6 +65,16 @@ import java.util.concurrent.TimeUnit; @Getter @Setter public class PlayerEntity extends LivingEntity { public static final float SNEAKING_POSE_HEIGHT = 1.5f; + protected static final List<AbilityLayer> BASE_ABILITY_LAYER; + + static { + AbilityLayer abilityLayer = new AbilityLayer(); + abilityLayer.setLayerType(AbilityLayer.Type.BASE); + Ability[] abilities = Ability.values(); + Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with + Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with + BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer); + } private String username; private boolean playerList = true; // Player is in the player list @@ -127,6 +136,7 @@ public class PlayerEntity extends LivingEntity { addPlayerPacket.setDeviceId(""); addPlayerPacket.setPlatformChatId(""); addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO + addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing addPlayerPacket.getMetadata().putFlags(flags); dirtyMetadata.apply(addPlayerPacket.getMetadata()); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java index 6c15a4d3e..176d171de 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java @@ -81,6 +81,7 @@ public class SkullPlayerEntity extends PlayerEntity { addPlayerPacket.setDeviceId(""); addPlayerPacket.setPlatformChatId(""); addPlayerPacket.setGameType(GameType.SURVIVAL); + addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); addPlayerPacket.getMetadata().putFlags(flags); dirtyMetadata.apply(addPlayerPacket.getMetadata()); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java index 56b6ee7ac..8f9eb415f 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java @@ -42,16 +42,21 @@ public class StoredItemMappings { private final ItemMapping banner; private final ItemMapping barrier; private final int bowl; + private final int bucket; private final int chest; private final ItemMapping compass; private final ItemMapping crossbow; private final ItemMapping enchantedBook; private final ItemMapping fishingRod; private final int flintAndSteel; + private final int frogspawn; + private final int goatHorn; + private final int glassBottle; private final int goldenApple; private final int goldIngot; private final int ironIngot; private final int lead; + private final int lilyPad; private final ItemMapping milkBucket; private final int nameTag; private final ItemMapping powderSnowBucket; @@ -70,16 +75,21 @@ public class StoredItemMappings { this.banner = load(itemMappings, "white_banner"); // As of 1.17.10, all banners have the same Bedrock ID this.barrier = load(itemMappings, "barrier"); this.bowl = load(itemMappings, "bowl").getJavaId(); + this.bucket = load(itemMappings, "bucket").getBedrockId(); this.chest = load(itemMappings, "chest").getJavaId(); this.compass = load(itemMappings, "compass"); this.crossbow = load(itemMappings, "crossbow"); this.enchantedBook = load(itemMappings, "enchanted_book"); this.fishingRod = load(itemMappings, "fishing_rod"); this.flintAndSteel = load(itemMappings, "flint_and_steel").getJavaId(); + this.frogspawn = load(itemMappings, "frogspawn").getBedrockId(); + this.goatHorn = load(itemMappings, "goat_horn").getJavaId(); + this.glassBottle = load(itemMappings, "glass_bottle").getBedrockId(); this.goldenApple = load(itemMappings, "golden_apple").getJavaId(); this.goldIngot = load(itemMappings, "gold_ingot").getJavaId(); this.ironIngot = load(itemMappings, "iron_ingot").getJavaId(); this.lead = load(itemMappings, "lead").getJavaId(); + this.lilyPad = load(itemMappings, "lily_pad").getBedrockId(); this.milkBucket = load(itemMappings, "milk_bucket"); this.nameTag = load(itemMappings, "name_tag").getJavaId(); this.powderSnowBucket = load(itemMappings, "powder_snow_bucket"); 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 a8d5859dc..58cbce77f 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 @@ -44,6 +44,7 @@ import java.util.Locale; * Used for block entities if the Java block state contains Bedrock block information. */ public final class BlockStateValues { + private static final IntSet ALL_CAULDRONS = new IntOpenHashSet(); private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap(); private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap(); private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap(); @@ -76,6 +77,8 @@ public final class BlockStateValues { public static int JAVA_SPAWNER_ID; public static int JAVA_WATER_ID; + public static final int NUM_WATER_LEVELS = 9; + /** * Determines if the block state contains Bedrock block information * @@ -193,6 +196,9 @@ public final class BlockStateValues { return; } + if (javaId.contains("cauldron")) { + ALL_CAULDRONS.add(javaBlockState); + } if (javaId.contains("_cauldron") && !javaId.contains("water_")) { NON_WATER_CAULDRONS.add(javaBlockState); } @@ -225,10 +231,19 @@ public final class BlockStateValues { * * @return if this Java block state is a non-empty non-water cauldron */ - public static boolean isCauldron(int state) { + public static boolean isNonWaterCauldron(int state) { return NON_WATER_CAULDRONS.contains(state); } + /** + * When using a bucket on a cauldron sending a ServerboundUseItemPacket can result in the liquid being placed. + * + * @return if this Java block state is a cauldron + */ + public static boolean isCauldron(int state) { + return ALL_CAULDRONS.contains(state); + } + /** * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags * in Bedrock need the conditional information. @@ -436,7 +451,6 @@ public final class BlockStateValues { /** * Get the level of water from the block state. - * This is used in FishingHookEntity to create splash sounds when the hook hits the water. * * @param state BlockState of the block * @return The water level or -1 if the block isn't water @@ -445,6 +459,30 @@ public final class BlockStateValues { return WATER_LEVEL.getOrDefault(state, -1); } + /** + * Get the height of water from the block state + * This is used in FishingHookEntity to create splash sounds when the hook hits the water. In addition, + * CollisionManager uses this to determine if the player's eyes are in water. + * + * @param state BlockState of the block + * @return The water height or -1 if the block does not contain water + */ + public static double getWaterHeight(int state) { + int waterLevel = BlockStateValues.getWaterLevel(state); + if (BlockRegistries.WATERLOGGED.get().contains(state)) { + waterLevel = 0; + } + if (waterLevel >= 0) { + double waterHeight = 1 - (waterLevel + 1) / ((double) NUM_WATER_LEVELS); + // Falling water is a full block + if (waterLevel >= 8) { + waterHeight = 1; + } + return waterHeight; + } + return -1; + } + /** * Get the slipperiness of a block. * This is used in ItemEntity to calculate the friction on an item as it slides across the ground diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java index 2b38e4ed4..2a830cd70 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.level.physics; +import com.nukkitx.math.GenericMath; import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; @@ -405,6 +406,18 @@ public class CollisionManager { return session.getGeyser().getWorldManager().getBlockAt(session, session.getPlayerEntity().getPosition().toInt()) == BlockStateValues.JAVA_WATER_ID; } + public boolean isWaterInEyes() { + double eyeX = playerBoundingBox.getMiddleX(); + double eyeY = playerBoundingBox.getMiddleY() - playerBoundingBox.getSizeY() / 2d + session.getEyeHeight(); + double eyeZ = playerBoundingBox.getMiddleZ(); + + eyeY -= 1 / ((double) BlockStateValues.NUM_WATER_LEVELS); // Subtract the height of one water layer + int blockID = session.getGeyser().getWorldManager().getBlockAt(session, GenericMath.floor(eyeX), GenericMath.floor(eyeY), GenericMath.floor(eyeZ)); + double waterHeight = BlockStateValues.getWaterHeight(blockID); + + return waterHeight != -1 && eyeY < (Math.floor(eyeY) + waterHeight); + } + /** * Updates scaffolding entity flags * Scaffolding needs to be checked per-move since it's a flag in Bedrock but Java does it client-side 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 33f2a8dc0..1d7ceaa00 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -29,6 +29,8 @@ import com.github.steveice10.mc.protocol.codec.MinecraftCodec; import com.github.steveice10.mc.protocol.codec.PacketCodec; import com.nukkitx.protocol.bedrock.BedrockPacketCodec; import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; +import com.nukkitx.protocol.bedrock.v534.Bedrock_v534; +import org.geysermc.geyser.session.GeyserSession; import java.util.ArrayList; import java.util.Collections; @@ -43,7 +45,7 @@ public final class GameProtocol { * Default Bedrock codec that should act as a fallback. Should represent the latest available * release of the game that Geyser supports. */ - public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v527.V527_CODEC; + public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v534.V534_CODEC; /** * A list of all supported Bedrock versions that can join Geyser */ @@ -56,9 +58,10 @@ public final class GameProtocol { private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC; static { - SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder() - .minecraftVersion("1.19.0") + SUPPORTED_BEDROCK_CODECS.add(Bedrock_v527.V527_CODEC.toBuilder() + .minecraftVersion("1.19.0/1.19.2") .build()); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } /** @@ -75,6 +78,12 @@ public final class GameProtocol { return null; } + /* Bedrock convenience methods to gatekeep features and easily remove the check on version removal */ + + public static boolean supports1_19_10(GeyserSession session) { + return session.getUpstream().getProtocolVersion() >= Bedrock_v534.V534_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 index f11851c1b..15a79dc76 100644 --- a/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java @@ -91,8 +91,10 @@ public class QueryPacketHandler { switch (type) { case HANDSHAKE: sendToken(); + break; case STATISTICS: sendQueryData(); + break; } } diff --git a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java index c3a242501..199e13918 100644 --- a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java +++ b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java @@ -28,6 +28,9 @@ package org.geysermc.geyser.ping; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.nukkitx.nbt.util.VarInts; +import io.netty.handler.codec.haproxy.HAProxyCommand; +import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; +import io.netty.util.NetUtil; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.network.GameProtocol; @@ -35,13 +38,12 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.net.ConnectException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketTimeoutException; +import java.net.*; import java.util.concurrent.TimeUnit; public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runnable { + private static final byte[] HAPROXY_BINARY_PREFIX = new byte[]{13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10}; + private final GeyserImpl geyser; public GeyserLegacyPingPassthrough(GeyserImpl geyser) { @@ -74,54 +76,68 @@ public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runn @Override public void run() { - try { - Socket socket = new Socket(); + try (Socket socket = new Socket()) { String address = geyser.getConfig().getRemote().getAddress(); int port = geyser.getConfig().getRemote().getPort(); socket.connect(new InetSocketAddress(address, port), 5000); ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(); - DataOutputStream handshake = new DataOutputStream(byteArrayStream); - handshake.write(0x0); - VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion()); - VarInts.writeUnsignedInt(handshake, address.length()); - handshake.writeBytes(address); - handshake.writeShort(port); - VarInts.writeUnsignedInt(handshake, 1); + try (DataOutputStream handshake = new DataOutputStream(byteArrayStream)) { + handshake.write(0x0); + VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion()); + VarInts.writeUnsignedInt(handshake, address.length()); + handshake.writeBytes(address); + handshake.writeShort(port); + VarInts.writeUnsignedInt(handshake, 1); + } - DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream()); - VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size()); - dataOutputStream.write(byteArrayStream.toByteArray()); - dataOutputStream.writeByte(0x01); - dataOutputStream.writeByte(0x00); + byte[] buffer; - DataInputStream dataInputStream = new DataInputStream(socket.getInputStream()); - VarInts.readUnsignedInt(dataInputStream); - VarInts.readUnsignedInt(dataInputStream); - int length = VarInts.readUnsignedInt(dataInputStream); - byte[] buffer = new byte[length]; - dataInputStream.readFully(buffer); - dataOutputStream.writeByte(0x09); - dataOutputStream.writeByte(0x01); - dataOutputStream.writeLong(System.currentTimeMillis()); + try (DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream())) { + if (geyser.getConfig().getRemote().isUseProxyProtocol()) { + // HAProxy support + // Based on https://github.com/netty/netty/blob/d8ad931488f6b942dabe28ecd6c399b4438da0a8/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageEncoder.java#L78 + dataOutputStream.write(HAPROXY_BINARY_PREFIX); + dataOutputStream.writeByte((0x02 << 4) | HAProxyCommand.PROXY.byteValue()); + dataOutputStream.writeByte(socket.getLocalAddress() instanceof Inet4Address ? + HAProxyProxiedProtocol.TCP4.byteValue() : HAProxyProxiedProtocol.TCP6.byteValue()); + byte[] srcAddrBytes = NetUtil.createByteArrayFromIpAddressString( + ((InetSocketAddress) socket.getLocalSocketAddress()).getAddress().getHostAddress()); + byte[] dstAddrBytes = NetUtil.createByteArrayFromIpAddressString(address); + dataOutputStream.writeShort(srcAddrBytes.length + dstAddrBytes.length + 4); + dataOutputStream.write(srcAddrBytes); + dataOutputStream.write(dstAddrBytes); + dataOutputStream.writeShort(((InetSocketAddress) socket.getLocalSocketAddress()).getPort()); + dataOutputStream.writeShort(port); + } - VarInts.readUnsignedInt(dataInputStream); - String json = new String(buffer); + VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size()); + dataOutputStream.write(byteArrayStream.toByteArray()); + dataOutputStream.writeByte(0x01); + dataOutputStream.writeByte(0x00); - this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(json, GeyserPingInfo.class); + try (DataInputStream dataInputStream = new DataInputStream(socket.getInputStream())) { + VarInts.readUnsignedInt(dataInputStream); + VarInts.readUnsignedInt(dataInputStream); + int length = VarInts.readUnsignedInt(dataInputStream); + buffer = new byte[length]; + dataInputStream.readFully(buffer); + dataOutputStream.writeByte(0x09); + dataOutputStream.writeByte(0x01); + dataOutputStream.writeLong(System.currentTimeMillis()); - byteArrayStream.close(); - handshake.close(); - dataOutputStream.close(); - dataInputStream.close(); - socket.close(); + VarInts.readUnsignedInt(dataInputStream); + } + } + + this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(buffer, GeyserPingInfo.class); } catch (SocketTimeoutException | ConnectException ex) { this.pingInfo = null; this.geyser.getLogger().debug("Connection timeout for ping passthrough."); } catch (JsonParseException | JsonMappingException ex) { this.geyser.getLogger().error("Failed to parse json when pinging server!", ex); } catch (IOException e) { - e.printStackTrace(); + this.geyser.getLogger().error("IO error while trying to use legacy ping passthrough", e); } } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java index 609647b2d..586e7d08b 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java @@ -72,6 +72,16 @@ public class BlockRegistries { */ public static final SimpleRegistry<IntSet> WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); + /** + * A registry containing all blockstates which are always interactive. + */ + public static final SimpleRegistry<IntSet> INTERACTIVE = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); + + /** + * A registry containing all blockstates which are interactive if the player has the may build permission. + */ + public static final SimpleRegistry<IntSet> INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new)); + static { BlockRegistryPopulator.populate(); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index 25528a919..53c3e2310 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.registry.populator; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.common.collect.ImmutableMap; import com.nukkitx.nbt.*; import com.nukkitx.protocol.bedrock.v527.Bedrock_v527; @@ -355,6 +356,24 @@ public class BlockRegistryPopulator { BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0])); BLOCKS_JSON = blocksJson; + + JsonNode blockInteractionsJson; + try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource("mappings/interactions.json")) { + blockInteractionsJson = GeyserImpl.JSON_MAPPER.readTree(stream); + } catch (Exception e) { + throw new AssertionError("Unable to load Java block interaction mappings", e); + } + + BlockRegistries.INTERACTIVE.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("always_consumes"))); + BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("requires_may_build"))); + } + + private static IntSet toBlockStateSet(ArrayNode node) { + IntSet blockStateSet = new IntOpenHashSet(node.size()); + for (JsonNode javaIdentifier : node) { + blockStateSet.add(BlockRegistries.JAVA_IDENTIFIERS.get().getInt(javaIdentifier.textValue())); + } + return blockStateSet; } private static NbtMap buildBedrockState(JsonNode node, int blockStateVersion, BiFunction<String, NbtMapBuilder, String> statesMapper) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index 22669fd79..9d6564fce 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -235,6 +235,9 @@ public class ItemRegistryPopulator { } else if (identifier.equals("minecraft:empty_map") && damage == 2) { // Bedrock-only as its own item continue; + } else if (identifier.equals("minecraft:bordure_indented_banner_pattern") || identifier.equals("minecraft:field_masoned_banner_pattern")) { + // Bedrock-only banner patterns + continue; } StartGamePacket.ItemEntry entry = entries.get(identifier); int id = -1; diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java index c4e967dff..ce7ac0b07 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java @@ -136,9 +136,9 @@ public class ItemMappings { } } else { if (!(mapping.getBedrockData() == data.getDamage() || - // Make exceptions for potions, tipped arrows, and firework stars, whose damage values can vary + // Make exceptions for potions, tipped arrows, firework stars, and goat horns, whose damage values can vary (mapping.getJavaIdentifier().endsWith("potion") || mapping.getJavaIdentifier().equals("minecraft:arrow") - || mapping.getJavaIdentifier().equals("minecraft:firework_star")))) { + || mapping.getJavaIdentifier().equals("minecraft:firework_star") || mapping.getJavaIdentifier().equals("minecraft:goat_horn")))) { continue; } } 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 99e29dd21..334549a50 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -80,6 +80,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.AccessLevel; @@ -101,6 +103,7 @@ import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.RemoteServer; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption; +import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; @@ -113,6 +116,7 @@ import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData; 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; @@ -386,7 +390,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * Whether to work around 1.13's different behavior in villager trading menus. */ @Setter - private boolean emulatePost1_14Logic = true; + private boolean emulatePost1_13Logic = true; /** * Starting in 1.17, Java servers expect the <code>carriedItem</code> parameter of the serverbound click container * packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot @@ -395,6 +399,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { */ @Setter private boolean emulatePost1_16Logic = true; + @Setter + private boolean emulatePost1_18Logic = true; /** * The current attack speed of the player. Used for sending proper cooldown timings. @@ -428,11 +434,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private long lastInteractionTime; /** - * Stores a future interaction to place a bucket. Will be cancelled if the client instead intended to - * interact with a block. + * Stores whether the player intended to place a bucket. */ @Setter - private ScheduledFuture<?> bucketScheduledFuture; + private boolean placedBucket; /** * Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. @@ -480,6 +485,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private boolean instabuild = false; + @Setter + private float flySpeed; + @Setter + private float walkSpeed; + /** * Caches current rain status. */ @@ -496,7 +506,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * Stores a map of all statistics sent from the server. * The server only sends new statistics back to us, so in order to show all statistics we need to cache existing ones. */ - private final Map<Statistic, Integer> statistics = new HashMap<>(); + private final Object2IntMap<Statistic> statistics = new Object2IntOpenHashMap<>(0); /** * Whether we're expecting statistics to be sent back to us. @@ -519,6 +529,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { */ private ScheduledFuture<?> tickThread = null; + /** + * Used to return the player to their original rotation after using an item in BedrockInventoryTransactionTranslator + */ + @Setter + private ScheduledFuture<?> lookBackScheduledFuture = null; + private MinecraftProtocol protocol; public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) { @@ -1265,9 +1281,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { ServerboundUseItemPacket useItemPacket; if (playerInventory.getItemInHand().getJavaId() == shield.getJavaId()) { - useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, getNextSequence()); + useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, worldCache.nextPredictionSequence()); } else if (playerInventory.getOffhand().getJavaId() == shield.getJavaId()) { - useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, getNextSequence()); + useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, worldCache.nextPredictionSequence()); } else { // No blocking return false; @@ -1296,7 +1312,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private boolean disableBlocking() { if (playerEntity.getFlag(EntityFlag.BLOCKING)) { ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, - Vector3i.ZERO, Direction.DOWN, getNextSequence()); + Vector3i.ZERO, Direction.DOWN, worldCache.nextPredictionSequence()); sendDownstreamPacket(releaseItemPacket); playerEntity.setFlag(EntityFlag.BLOCKING, false); return true; @@ -1610,23 +1626,83 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return geyser.getWorldManager().hasPermission(this, permission); } + private static final Ability[] USED_ABILITIES = Ability.values(); + /** * Send an AdventureSettingsPacket to the client with the latest flags */ public void sendAdventureSettings() { - AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket(); - adventureSettingsPacket.setUniqueEntityId(playerEntity.getGeyserId()); + long bedrockId = playerEntity.getGeyserId(); // Set command permission if OP permission level is high enough // This allows mobile players access to a GUI for doing commands. The commands there do not change above OPERATOR // and all commands there are accessible with OP permission level 2 - adventureSettingsPacket.setCommandPermission(opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL); + CommandPermission commandPermission = opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL; // Required to make command blocks destroyable - adventureSettingsPacket.setPlayerPermission(opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER); + PlayerPermission playerPermission = opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER; // Update the noClip and worldImmutable values based on the current gamemode boolean spectator = gameMode == GameMode.SPECTATOR; boolean worldImmutable = gameMode == GameMode.ADVENTURE || spectator; + if (GameProtocol.supports1_19_10(this)) { + UpdateAdventureSettingsPacket adventureSettingsPacket = new UpdateAdventureSettingsPacket(); + adventureSettingsPacket.setNoMvP(false); + adventureSettingsPacket.setNoPvM(false); + adventureSettingsPacket.setImmutableWorld(worldImmutable); + adventureSettingsPacket.setShowNameTags(false); + adventureSettingsPacket.setAutoJump(true); + sendUpstreamPacket(adventureSettingsPacket); + + UpdateAbilitiesPacket updateAbilitiesPacket = new UpdateAbilitiesPacket(); + updateAbilitiesPacket.setUniqueEntityId(bedrockId); + updateAbilitiesPacket.setCommandPermission(commandPermission); + updateAbilitiesPacket.setPlayerPermission(playerPermission); + + AbilityLayer abilityLayer = new AbilityLayer(); + Set<Ability> abilities = abilityLayer.getAbilityValues(); + if (canFly || spectator) { + abilities.add(Ability.MAY_FLY); + } + + // Default stuff we have to fill in + abilities.add(Ability.BUILD); + abilities.add(Ability.MINE); + // Needed so you can drop items + abilities.add(Ability.DOORS_AND_SWITCHES); + if (gameMode == GameMode.CREATIVE) { + // Needed so the client doesn't attempt to take away items + abilities.add(Ability.INSTABUILD); + } + + if (flying || spectator) { + if (spectator && !flying) { + // We're "flying locked" in this gamemode + flying = true; + ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(true); + sendDownstreamPacket(abilitiesPacket); + } + abilities.add(Ability.FLYING); + } + + if (spectator) { + abilities.add(Ability.NO_CLIP); + } + + abilityLayer.setLayerType(AbilityLayer.Type.BASE); + abilityLayer.setFlySpeed(flySpeed); + abilityLayer.setWalkSpeed(walkSpeed); + Collections.addAll(abilityLayer.getAbilitiesSet(), USED_ABILITIES); + + updateAbilitiesPacket.getAbilityLayers().add(abilityLayer); + sendUpstreamPacket(updateAbilitiesPacket); + return; + } + + AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket(); + adventureSettingsPacket.setUniqueEntityId(bedrockId); + adventureSettingsPacket.setCommandPermission(commandPermission); + adventureSettingsPacket.setPlayerPermission(playerPermission); + Set<AdventureSetting> flags = adventureSettingsPacket.getSettings(); if (canFly || spectator) { flags.add(AdventureSetting.MAY_FLY); @@ -1676,16 +1752,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { sendDownstreamPacket(clientSettingsPacket); } - public int getNextSequence() { - return 0; - } - /** * Used for updating statistic values since we only get changes from the server * * @param statistics Updated statistics values */ - public void updateStatistics(@NonNull Map<Statistic, Integer> statistics) { + public void updateStatistics(@Nonnull Object2IntMap<Statistic> statistics) { if (this.statistics.isEmpty()) { // Initialize custom statistics to 0, so that they appear in the form for (CustomStatistic customStatistic : CustomStatistic.values()) { @@ -1757,6 +1829,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { sendUpstreamPacket(packet); } + public float getEyeHeight() { + return switch (pose) { + case SNEAKING -> 1.27f; + case SWIMMING, + FALL_FLYING, // Elytra + SPIN_ATTACK -> 0.4f; // Trident spin attack + case SLEEPING -> 0.2f; + default -> EntityDefinitions.PLAYER.offset(); + }; + } + public MinecraftCodecHelper getCodecHelper() { return (MinecraftCodecHelper) this.downstream.getCodecHelper(); } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index d46a39616..ac0c93204 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.session.cache; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntLists; +import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.registry.type.ItemMapping; @@ -82,6 +83,15 @@ public class TagCache { this.requiresIronTool = IntList.of(blockTags.get("minecraft:needs_iron_tool")); this.requiresDiamondTool = IntList.of(blockTags.get("minecraft:needs_diamond_tool")); + // Hack btw + GeyserLogger logger = session.getGeyser().getLogger(); + int[] convertableToMud = blockTags.get("minecraft:convertable_to_mud"); + boolean emulatePost1_18Logic = convertableToMud != null && convertableToMud.length != 0; + session.setEmulatePost1_18Logic(emulatePost1_18Logic); + if (logger.isDebug()) { + logger.debug("Emulating post 1.18 block predication logic for " + session.name() + "? " + emulatePost1_18Logic); + } + Map<String, int[]> itemTags = packet.getTags().get("minecraft:item"); this.axolotlTemptItems = IntList.of(itemTags.get("minecraft:axolotl_tempt_items")); this.fishes = IntList.of(itemTags.get("minecraft:fishes")); @@ -91,10 +101,10 @@ public class TagCache { this.smallFlowers = IntList.of(itemTags.get("minecraft:small_flowers")); // Hack btw - boolean emulatePost1_14Logic = itemTags.get("minecraft:signs").length > 1; - session.setEmulatePost1_14Logic(emulatePost1_14Logic); - if (session.getGeyser().getLogger().isDebug()) { - session.getGeyser().getLogger().debug("Emulating post 1.14 villager logic for " + session.name() + "? " + emulatePost1_14Logic); + boolean emulatePost1_13Logic = itemTags.get("minecraft:signs").length > 1; + session.setEmulatePost1_13Logic(emulatePost1_13Logic); + if (logger.isDebug()) { + logger.debug("Emulating post 1.13 villager logic for " + session.name() + "? " + emulatePost1_13Logic); } } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java index 17679ad3e..239f5c865 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java @@ -26,12 +26,18 @@ package org.geysermc.geyser.session.cache; import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; +import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.packet.SetTitlePacket; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.scoreboard.Scoreboard; import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.ChunkUtils; + +import java.util.Iterator; +import java.util.Map; public final class WorldCache { private final GeyserSession session; @@ -51,6 +57,9 @@ public final class WorldCache { private int trueTitleStayTime; private int trueTitleFadeOutTime; + private int currentSequence; + private final Map<Vector3i, ServerVerifiedState> unverifiedPredictions = new Object2ObjectOpenHashMap<>(1); + public WorldCache(GeyserSession session) { this.session = session; this.scoreboard = new Scoreboard(session); @@ -121,4 +130,75 @@ public final class WorldCache { forceSyncCorrectTitleTimes(); } } + + /* Code to support the prediction structure introduced in Java Edition 1.19.0 + Blocks can be rolled back if invalid, but this requires some client-side information storage. */ + + public int nextPredictionSequence() { + return ++currentSequence; + } + + /** + * Stores a record of a block at a certain position to rollback in the event it is incorrect. + */ + public void addServerCorrectBlockState(Vector3i position, int blockState) { + if (session.isEmulatePost1_18Logic()) { + // Cheap hack + // On non-Bukkit platforms, ViaVersion will always confirm the sequence before the block is updated, + // meaning we'd send two block updates after (ChunkUtils.updateBlockClientSide in endPredictionsUpTo + // and the packet updating from the client) + this.unverifiedPredictions.compute(position, ($, serverVerifiedState) -> serverVerifiedState == null + ? new ServerVerifiedState(currentSequence, blockState) : serverVerifiedState.setData(currentSequence, blockState)); + } + } + + public void updateServerCorrectBlockState(Vector3i position) { + if (this.unverifiedPredictions.isEmpty()) { + return; + } + + this.unverifiedPredictions.remove(position); + } + + public void endPredictionsUpTo(int sequence) { + if (this.unverifiedPredictions.isEmpty()) { + return; + } + + Iterator<Map.Entry<Vector3i, ServerVerifiedState>> it = this.unverifiedPredictions.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<Vector3i, ServerVerifiedState> entry = it.next(); + ServerVerifiedState serverVerifiedState = entry.getValue(); + if (serverVerifiedState.sequence <= sequence) { + // This block may be out of sync with the server + // In 1.19.0 Java, you can verify this by trying to mine in spawn protection + ChunkUtils.updateBlockClientSide(session, serverVerifiedState.blockState, entry.getKey()); + it.remove(); + } + } + } + + private static class ServerVerifiedState { + private int sequence; + private int blockState; + + ServerVerifiedState(int sequence, int blockState) { + this.sequence = sequence; + this.blockState = blockState; + } + + ServerVerifiedState setData(int sequence, int blockState) { + this.sequence = sequence; + this.blockState = blockState; + return this; + } + + @Override + public String toString() { + return "ServerVerifiedState{" + + "sequence=" + sequence + + ", blockState=" + blockState + + '}'; + } + } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java index 800eb6c0f..ad2514e09 100644 --- a/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java +++ b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.text; -import com.github.steveice10.mc.protocol.data.game.MessageType; +import com.github.steveice10.mc.protocol.data.game.BuiltinChatType; import com.nukkitx.protocol.bedrock.packet.TextPacket; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -45,13 +45,13 @@ public record ChatTypeEntry(@Nonnull TextPacket.Type bedrockChatType, @Nullable // 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); + chatTypes.put(BuiltinChatType.CHAT.ordinal(), CHAT); + chatTypes.put(BuiltinChatType.SYSTEM.ordinal(), SYSTEM); + chatTypes.put(BuiltinChatType.GAME_INFO.ordinal(), TIP); + chatTypes.put(BuiltinChatType.SAY_COMMAND.ordinal(), RAW); + chatTypes.put(BuiltinChatType.MSG_COMMAND.ordinal(), RAW); + chatTypes.put(BuiltinChatType.TEAM_MSG_COMMAND.ordinal(), RAW); + chatTypes.put(BuiltinChatType.EMOTE_COMMAND.ordinal(), RAW); + chatTypes.put(BuiltinChatType.TELLRAW_COMMAND.ordinal(), RAW); } } diff --git a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java index e5f3c0554..94ad5eead 100644 --- a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java @@ -126,14 +126,20 @@ public class MinecraftLocale { // Check the locale isn't already loaded if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) { - GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.invalid", locale)); + if (loadLocale(locale)) { + GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale); + } else { + GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.invalid", locale)); + } return; } GeyserImpl.getInstance().getLogger().debug("Downloading and loading locale: " + locale); downloadLocale(locale); - loadLocale(locale); + if (!loadLocale(locale)) { + GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.missing", locale)); + } } /** @@ -199,7 +205,7 @@ public class MinecraftLocale { * * @param locale Locale to load */ - private static void loadLocale(String locale) { + private static boolean loadLocale(String locale) { File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile(); // Load the locale @@ -242,8 +248,9 @@ public class MinecraftLocale { } catch (IOException e) { throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.locale.fail.file", locale, e.getMessage())); } + return true; } else { - GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.missing", locale)); + return false; } } @@ -300,9 +307,9 @@ public class MinecraftLocale { * @return Translated string or the original message if it was not found in the given locale */ public static String getLocaleString(String messageText, String locale) { - Map<String, String> localeStrings = MinecraftLocale.LOCALE_MAPPINGS.get(locale.toLowerCase()); + Map<String, String> localeStrings = LOCALE_MAPPINGS.get(locale.toLowerCase(Locale.ROOT)); if (localeStrings == null) { - localeStrings = MinecraftLocale.LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale()); + localeStrings = LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale()); if (localeStrings == null) { // Don't cause a NPE if the locale is STILL missing GeyserImpl.getInstance().getLogger().debug("MISSING DEFAULT LOCALE: " + GeyserLocale.getDefaultLocale()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java index 54dc533c6..4dac5e86f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java @@ -119,7 +119,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator } private OptionalInt toJava(int effectChoice) { - return effectChoice == -1 ? OptionalInt.empty() : OptionalInt.of(effectChoice); + return effectChoice == 0 ? OptionalInt.empty() : OptionalInt.of(effectChoice); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java index a7b736d72..5a237b72a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java @@ -60,7 +60,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator { static { // Added from left-to-right then up-to-down in the order Java presents it - int index = 1; + int index = 0; PATTERN_TO_INDEX.put("bl", index++); PATTERN_TO_INDEX.put("br", index++); PATTERN_TO_INDEX.put("tl", index++); @@ -119,15 +119,16 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator { @Override protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { // If the LOOM_MATERIAL slot is not empty, we are crafting a pattern that does not come from an item - // Remove the CRAFT_NON_IMPLEMENTED_DEPRECATED when 1.17.30 is dropped - return (action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED || action.getType() == StackRequestActionType.CRAFT_LOOM) - && inventory.getItem(2).isEmpty(); + return action.getType() == StackRequestActionType.CRAFT_LOOM && inventory.getItem(2).isEmpty(); } @Override public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { StackRequestActionData headerData = request.getActions()[0]; StackRequestActionData data = request.getActions()[1]; + if (!(headerData instanceof CraftLoomStackRequestActionData)) { + return rejectRequest(request); + } if (!(data instanceof CraftResultsDeprecatedStackRequestActionData craftData)) { return rejectRequest(request); } @@ -136,15 +137,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator { List<NbtMap> newBlockEntityTag = craftData.getResultItems()[0].getTag().getList("Patterns", NbtType.COMPOUND); // Get the pattern that the Bedrock client requests - the last pattern in the Patterns list NbtMap pattern = newBlockEntityTag.get(newBlockEntityTag.size() - 1); - String bedrockPattern; - - if (headerData instanceof CraftLoomStackRequestActionData loomData) { - // Prioritize this if on 1.17.40 - // Remove the below if statement when 1.17.30 is dropped - bedrockPattern = loomData.getPatternId(); - } else { - bedrockPattern = pattern.getString("Pattern"); - } + String bedrockPattern = ((CraftLoomStackRequestActionData) headerData).getPatternId(); // Get the Java index of this pattern int index = PATTERN_TO_INDEX.getOrDefault(bedrockPattern, -1); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java index d4bac172c..5e9c99ae9 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java @@ -155,7 +155,7 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator { ServerboundSelectTradePacket packet = new ServerboundSelectTradePacket(tradeChoice); session.sendDownstreamPacket(packet); - if (session.isEmulatePost1_14Logic()) { + if (session.isEmulatePost1_13Logic()) { // 1.18 Java cooperates nicer than older versions if (inventory instanceof MerchantContainer merchantInventory) { merchantInventory.onTradeSelected(session, tradeChoice); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/GoatHornTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/GoatHornTranslator.java new file mode 100644 index 000000000..2cb9d7ec7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/GoatHornTranslator.java @@ -0,0 +1,98 @@ +/* + * 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.translator.inventory.item; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.network.GameProtocol; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.registry.type.ItemMappings; + +import java.util.Collections; +import java.util.List; + +@ItemRemapper +public class GoatHornTranslator extends ItemTranslator { + + private static final List<String> INSTRUMENTS = List.of( + "ponder_goat_horn", + "sing_goat_horn", + "seek_goat_horn", + "feel_goat_horn", + "admire_goat_horn", + "call_goat_horn", + "yearn_goat_horn", + "dream_goat_horn" // Called "Resist" on Bedrock 1.19.0 due to https://bugs.mojang.com/browse/MCPE-155059 + ); + + @Override + protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) { + ItemData.Builder builder = super.translateToBedrock(itemStack, mapping, mappings); + if (itemStack.getNbt() != null && itemStack.getNbt().get("instrument") instanceof StringTag instrumentTag) { + String instrument = instrumentTag.getValue(); + // Drop the Minecraft namespace if applicable + if (instrument.startsWith("minecraft:")) { + instrument = instrument.substring("minecraft:".length()); + } + + int damage = INSTRUMENTS.indexOf(instrument); + if (damage == -1) { + damage = 0; + GeyserImpl.getInstance().getLogger().debug("Unknown goat horn instrument: " + instrumentTag.getValue()); + } + builder.damage(damage); + } + return builder; + } + + @Override + public ItemStack translateToJava(ItemData itemData, ItemMapping mapping, ItemMappings mappings) { + ItemStack itemStack = super.translateToJava(itemData, mapping, mappings); + + int damage = itemData.getDamage(); + if (damage < 0 || damage >= INSTRUMENTS.size()) { + GeyserImpl.getInstance().getLogger().debug("Unknown goat horn instrument for damage: " + damage); + damage = 0; + } + + String instrument = INSTRUMENTS.get(damage); + StringTag instrumentTag = new StringTag("instrument", "minecraft:" + instrument); + itemStack.getNbt().put(instrumentTag); + + return itemStack; + } + + @Override + public List<ItemMapping> getAppliedItems() { + return Collections.singletonList( + Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) + .getMapping("minecraft:goat_horn") + ); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java index 94760b66c..9ae3300cd 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java @@ -62,7 +62,7 @@ public interface BedrockOnlyBlockEntity extends RequiresBlockState { return FlowerPotBlockEntityTranslator.getTag(session, blockState, position); } else if (PistonBlockEntityTranslator.isBlock(blockState)) { return PistonBlockEntityTranslator.getTag(blockState, position); - } else if (BlockStateValues.isCauldron(blockState)) { + } else if (BlockStateValues.isNonWaterCauldron(blockState)) { // As of 1.18.30: this is required to make rendering not look weird on chunk load (lava and snow cauldrons look dim) return NbtMap.builder() .putString("id", "Cauldron") diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 243b1cede..24c046ef2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -33,17 +33,22 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*; +import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.inventory.*; -import com.nukkitx.protocol.bedrock.packet.*; +import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; +import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket; +import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; @@ -53,6 +58,8 @@ import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; +import org.geysermc.geyser.translator.inventory.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.BlockUtils; @@ -122,7 +129,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM, Vector3i.ZERO, Direction.DOWN, - session.getNextSequence() + session.getWorldCache().nextPredictionSequence() ); session.sendDownstreamPacket(dropPacket); @@ -170,6 +177,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve session.setLastInteractionTime(System.currentTimeMillis()); } + if (isIncorrectHeldItem(session, packet)) { + restoreCorrectBlock(session, blockPos, packet); + return; + } + // Bedrock sends block interact code for a Java entity so we send entity code back to Java if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) { Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition()); @@ -192,18 +204,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve // CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch Vector3f playerPosition = session.getPlayerEntity().getPosition(); - - // Adjust position for current eye height - switch (session.getPose()) { - case SNEAKING -> - playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 1.27f), 0); - case SWIMMING, - FALL_FLYING, // Elytra - SPIN_ATTACK -> // Trident spin attack - playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.4f), 0); - case SLEEPING -> - playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.2f), 0); - } // else, we don't have to modify the position + playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight()); boolean creative = session.getGameMode() == GameMode.CREATIVE; @@ -255,9 +256,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()); if (blockState == BlockStateValues.JAVA_WATER_ID) { // Otherwise causes multiple mobs to spawn - just send a use item packet - // TODO when we fix mobile bucket rotation, use it for this, too - ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); - session.sendDownstreamPacket(itemPacket); + useItem(session, packet, blockState); break; } } @@ -268,33 +267,33 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve Hand.MAIN_HAND, packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(), false, - session.getNextSequence()); + session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(blockPacket); if (packet.getItemInHand() != null) { - // Otherwise boats will not be able to be placed in survival and buckets won't work on mobile - if (session.getItemMappings().getBoatIds().contains(packet.getItemInHand().getId())) { - ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); - session.sendDownstreamPacket(itemPacket); - } else if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId())) { - // Let the server decide if the bucket item should change, not the client, and revert the changes the client made - InventorySlotPacket slotPacket = new InventorySlotPacket(); - slotPacket.setContainerId(ContainerId.INVENTORY); - slotPacket.setSlot(packet.getHotbarSlot()); - slotPacket.setItem(packet.getItemInHand()); - session.sendUpstreamPacket(slotPacket); + int itemId = packet.getItemInHand().getId(); + int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()); + // Otherwise boats will not be able to be placed in survival and buckets, lily pads, frogspawn, and glass bottles won't work on mobile + if (session.getItemMappings().getBoatIds().contains(itemId) || + itemId == session.getItemMappings().getStoredItems().lilyPad() || + itemId == session.getItemMappings().getStoredItems().frogspawn()) { + useItem(session, packet, blockState); + } else if (itemId == session.getItemMappings().getStoredItems().glassBottle()) { + if (!session.isSneaking() && BlockStateValues.isCauldron(blockState) && !BlockStateValues.isNonWaterCauldron(blockState)) { + // ServerboundUseItemPacket is not sent for water cauldrons and glass bottles + return; + } + useItem(session, packet, blockState); + } else if (session.getItemMappings().getBucketIds().contains(itemId)) { // Don't send ServerboundUseItemPacket for powder snow buckets - if (packet.getItemInHand().getId() != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) { - // Special check for crafting tables since clients don't send BLOCK_INTERACT when interacting - int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()); - if (session.isSneaking() || blockState != BlockRegistries.JAVA_IDENTIFIERS.get("minecraft:crafting_table")) { - // Delay the interaction in case the client doesn't intend to actually use the bucket - // See BedrockActionTranslator.java - session.setBucketScheduledFuture(session.scheduleInEventLoop(() -> { - ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); - session.sendDownstreamPacket(itemPacket); - }, 5, TimeUnit.MILLISECONDS)); + if (itemId != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) { + if (!session.isSneaking() && BlockStateValues.isCauldron(blockState)) { + // ServerboundUseItemPacket is not sent for cauldrons and buckets + return; } + session.setPlacedBucket(useItem(session, packet, blockState)); + } else { + session.setPlacedBucket(true); } } } @@ -320,6 +319,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve session.setInteracting(true); } case 1 -> { + if (isIncorrectHeldItem(session, packet)) { + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot())); + break; + } + // Handled when sneaking if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) { break; @@ -334,10 +338,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve } else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) { // Handled in case 0 break; + } else if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) { + // Handled in case 0 + break; } } - ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence()); + ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(useItemPacket); List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots(); @@ -402,12 +409,22 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve return; } + int sequence = session.getWorldCache().nextPredictionSequence(); + if (blockState != -1) { + session.getWorldCache().addServerCorrectBlockState(packet.getBlockPosition(), blockState); + } else { + blockState = BlockStateValues.JAVA_AIR_ID; + // Client will desync here anyway + session.getWorldCache().addServerCorrectBlockState(packet.getBlockPosition(), + session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition())); + } + LevelEventPacket blockBreakPacket = new LevelEventPacket(); blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK); blockBreakPacket.setPosition(packet.getBlockPosition().toFloat()); blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState)); session.sendUpstreamPacket(blockBreakPacket); - session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID); + session.setBreakingBlock(-1); Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition()); if (itemFrameEntity != null) { @@ -418,7 +435,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve } PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING; - ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], session.getNextSequence()); + ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], sequence); session.sendDownstreamPacket(breakPacket); } } @@ -427,7 +444,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve if (packet.getActionType() == 0) { // Followed to the Minecraft Protocol specification outlined at wiki.vg ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO, - Direction.DOWN, session.getNextSequence()); + Direction.DOWN, session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(releaseItemPacket); } break; @@ -520,10 +537,117 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve session.sendUpstreamPacket(updateWaterPacket); // Reset the item in hand to prevent "missing" blocks - InventorySlotPacket slotPacket = new InventorySlotPacket(); - slotPacket.setContainerId(ContainerId.INVENTORY); - slotPacket.setSlot(packet.getHotbarSlot()); - slotPacket.setItem(packet.getItemInHand()); - session.sendUpstreamPacket(slotPacket); + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot())); + } + + private boolean isIncorrectHeldItem(GeyserSession session, InventoryTransactionPacket packet) { + int javaSlot = session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()); + int expectedItemId = ItemTranslator.getBedrockItemMapping(session, session.getPlayerInventory().getItem(javaSlot)).getBedrockId(); + int heldItemId = packet.getItemInHand() == null ? ItemData.AIR.getId() : packet.getItemInHand().getId(); + + if (expectedItemId != heldItemId) { + session.getGeyser().getLogger().debug(session.name() + "'s held item has desynced! Expected: " + expectedItemId + " Received: " + heldItemId); + session.getGeyser().getLogger().debug("Packet: " + packet); + return true; + } + return false; + } + + private boolean useItem(GeyserSession session, InventoryTransactionPacket packet, int blockState) { + // Update the player's inventory to remove any items added by the client itself + Inventory playerInventory = session.getPlayerInventory(); + int heldItemSlot = playerInventory.getOffsetForHotbar(packet.getHotbarSlot()); + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, heldItemSlot); + if (playerInventory.getItem(heldItemSlot).getAmount() > 1) { + if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().bucket() || + packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) { + // Using a stack of buckets or glass bottles will result in an item being added to the first empty slot. + // We need to revert the item in case the interaction fails. The order goes from left to right in the + // hotbar. Then left to right and top to bottom in the inventory. + for (int i = 0; i < 36; i++) { + int slot = i; + if (i < 9) { + slot = playerInventory.getOffsetForHotbar(slot); + } + if (playerInventory.getItem(slot).isEmpty()) { + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, slot); + break; + } + } + } + } + // Check if the player is interacting with a block + if (!session.isSneaking()) { + if (BlockRegistries.INTERACTIVE.get().contains(blockState)) { + return false; + } + + boolean mayBuild = session.getGameMode() == GameMode.SURVIVAL || session.getGameMode() == GameMode.CREATIVE; + if (mayBuild && BlockRegistries.INTERACTIVE_MAY_BUILD.get().contains(blockState)) { + return false; + } + } + + Vector3f target = packet.getBlockPosition().toFloat().add(packet.getClickPosition()); + lookAt(session, target); + + ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence()); + session.sendDownstreamPacket(itemPacket); + return true; + } + + /** + * Determine the rotation necessary to activate this transaction. + * + * The position between the intended click position and the player can be determined with two triangles. + * First, we compute the difference of the X and Z coordinates: + * + * Player position (0, 0) + * | + * | + * | + * |_____________ Intended target (-3, 2) + * + * We then use the Pythagorean Theorem to find the direct line (hypotenuse) on the XZ plane. Finding the angle of the + * triangle from there, closest to the player, gives us our yaw rotation value + * Then doing the same using the new XZ distance and Y difference, we can find the direct line of sight from the + * player to the intended target, and the pitch rotation value. We can then send the necessary packets to update + * the player's rotation. + * + * @param session the Geyser Session + * @param target the position to look at + */ + private void lookAt(GeyserSession session, Vector3f target) { + // Use the bounding box's position since we need the player's position seen by the Java server + Vector3d playerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter(); + float xDiff = (float) (target.getX() - playerPosition.getX()); + float yDiff = (float) (target.getY() - (playerPosition.getY() + session.getEyeHeight())); + float zDiff = (float) (target.getZ() - playerPosition.getZ()); + + // First triangle on the XZ plane + float yaw = (float) -Math.toDegrees(Math.atan2(xDiff, zDiff)); + // Second triangle on the Y axis using the hypotenuse of the first triangle as a side + double xzHypot = Math.sqrt(xDiff * xDiff + zDiff * zDiff); + float pitch = (float) -Math.toDegrees(Math.atan2(yDiff, xzHypot)); + + SessionPlayerEntity entity = session.getPlayerEntity(); + ServerboundMovePlayerPosRotPacket returnPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), entity.getYaw(), entity.getPitch()); + // This matches Java edition behavior + ServerboundMovePlayerPosRotPacket movementPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), yaw, pitch); + session.sendDownstreamPacket(movementPacket); + + if (session.getLookBackScheduledFuture() != null) { + session.getLookBackScheduledFuture().cancel(false); + } + if (Math.abs(entity.getYaw() - yaw) > 1f || Math.abs(entity.getPitch() - pitch) > 1f) { + session.setLookBackScheduledFuture(session.scheduleInEventLoop(() -> { + Vector3d newPlayerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter(); + if (!newPlayerPosition.equals(playerPosition) || entity.getYaw() != returnPacket.getYaw() || entity.getPitch() != returnPacket.getPitch()) { + // The player moved/rotated so there is no need to change their rotation back + return; + } + session.sendDownstreamPacket(returnPacket); + }, 150, TimeUnit.MILLISECONDS)); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java index 499a54322..25a579dc7 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java @@ -57,7 +57,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda Hand.MAIN_HAND, 0, 0, 0, // Java doesn't care about these when dealing with a lectern false, - session.getNextSequence()); + session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(blockPacket); } else { // Bedrock wants to either move a page or exit diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java index 1875e8fe5..b8decad78 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java @@ -65,7 +65,7 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment // Activate shield since we are already sneaking // (No need to send a release item packet - Java doesn't do this when swapping items) // Required to do it a tick later or else it doesn't register - session.scheduleInEventLoop(() -> session.sendDownstreamPacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence())), + session.scheduleInEventLoop(() -> session.sendDownstreamPacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence())), 50, TimeUnit.MILLISECONDS); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java index fe519c329..5001fc2d2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java @@ -41,7 +41,6 @@ import com.nukkitx.protocol.bedrock.packet.*; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; -import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -129,21 +128,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket break; case DROP_ITEM: ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, - vector, Direction.VALUES[packet.getFace()], session.getNextSequence()); + vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(dropItemPacket); break; case STOP_SLEEP: ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED); session.sendDownstreamPacket(stopSleepingPacket); break; - case BLOCK_INTERACT: - // Client means to interact with a block; cancel bucket interaction, if any - if (session.getBucketScheduledFuture() != null) { - session.getBucketScheduledFuture().cancel(true); - session.setBucketScheduledFuture(null); - } - // Otherwise handled in BedrockInventoryTransactionTranslator - break; case START_BREAK: // Start the block breaking animation if (session.getGameMode() != GameMode.CREATIVE) { @@ -163,7 +154,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket String identifier = BlockRegistries.JAVA_IDENTIFIERS.get().get(blockUp); if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) { ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos, - Direction.VALUES[packet.getFace()], session.getNextSequence()); + Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(startBreakingPacket); if (session.getGameMode() == GameMode.CREATIVE) { break; @@ -171,17 +162,22 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket } ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, - vector, Direction.VALUES[packet.getFace()], session.getNextSequence()); + vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(startBreakingPacket); break; case CONTINUE_BREAK: if (session.getGameMode() == GameMode.CREATIVE) { break; } + int breakingBlock = session.getBreakingBlock(); + if (breakingBlock == -1) { + break; + } + Vector3f vectorFloat = vector.toFloat(); LevelEventPacket continueBreakPacket = new LevelEventPacket(); continueBreakPacket.setType(LevelEventType.PARTICLE_CRACK_BLOCK); - continueBreakPacket.setData((session.getBlockMappings().getBedrockBlockId(session.getBreakingBlock())) | (packet.getFace() << 24)); + continueBreakPacket.setData((session.getBlockMappings().getBedrockBlockId(breakingBlock)) | (packet.getFace() << 24)); continueBreakPacket.setPosition(vectorFloat); session.sendUpstreamPacket(continueBreakPacket); @@ -189,7 +185,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket LevelEventPacket updateBreak = new LevelEventPacket(); updateBreak.setType(LevelEventType.BLOCK_UPDATE_BREAK); updateBreak.setPosition(vectorFloat); - double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(session.getBreakingBlock())) * 20; + double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(breakingBlock)) * 20; updateBreak.setData((int) (65535 / breakTime)); session.sendUpstreamPacket(updateBreak); break; @@ -206,13 +202,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket } } - ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, session.getNextSequence()); + ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(abortBreakingPacket); LevelEventPacket stopBreak = new LevelEventPacket(); stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK); stopBreak.setPosition(vector.toFloat()); stopBreak.setData(0); - session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID); + session.setBreakingBlock(-1); session.sendUpstreamPacket(stopBreak); break; case STOP_BREAK: diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java index 187f6a98b..5d15761bd 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java @@ -44,7 +44,7 @@ public class BedrockEmoteTranslator extends PacketTranslator<EmotePacket> { if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) { // Activate the workaround - we should trigger the offhand now ServerboundPlayerActionPacket swapHandsPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO, - Direction.DOWN, session.getNextSequence()); + Direction.DOWN, session.getWorldCache().nextPredictionSequence()); session.sendDownstreamPacket(swapHandsPacket); if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java index 6926d33d2..b9f593961 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -77,6 +77,13 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack boolean positionChanged = !entity.getPosition().equals(packet.getPosition()); boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw; + if (session.getLookBackScheduledFuture() != null) { + // Resend the rotation if it was changed by Geyser + rotationChanged |= !session.getLookBackScheduledFuture().isDone(); + session.getLookBackScheduledFuture().cancel(false); + session.setLookBackScheduledFuture(null); + } + // If only the pitch and yaw changed // This isn't needed, but it makes the packets closer to vanilla // It also means you can't "lag back" while only looking, in theory diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java index ebe70a856..356fe645b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java @@ -50,6 +50,8 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator<ClientboundL if (disconnectReason instanceof TranslatableComponent component) { String key = component.key(); isOutdatedMessage = "multiplayer.disconnect.incompatible".equals(key) || + // Seen with Velocity 1.18 rejecting a 1.19 client + "multiplayer.disconnect.outdated_client".equals(key) || // Legacy string (starting from at least 1.15.2) "multiplayer.disconnect.outdated_server".equals(key) // Reproduced on 1.15.2 server with ViaVersion 4.0.0-21w20a with 1.18.2 Java client 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 1c83bf2cc..b042eae2a 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 @@ -25,7 +25,7 @@ package org.geysermc.geyser.translator.protocol.java; -import com.github.steveice10.mc.protocol.data.game.MessageType; +import com.github.steveice10.mc.protocol.data.game.BuiltinChatType; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; @@ -82,14 +82,15 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket textDecoration = new TextDecoration(decorationTag); } } - MessageType type = MessageType.from(((StringTag) tag.get("name")).getValue()); + BuiltinChatType type = BuiltinChatType.from(((StringTag) tag.get("name")).getValue()); // TODO new types? - TextPacket.Type bedrockType = switch (type) { + // The built-in type can be null if custom plugins/mods add in new types + TextPacket.Type bedrockType = type != null ? switch (type) { case CHAT -> TextPacket.Type.CHAT; case SYSTEM -> TextPacket.Type.SYSTEM; case GAME_INFO -> TextPacket.Type.TIP; default -> TextPacket.Type.RAW; - }; + } : TextPacket.Type.RAW; chatTypes.put(id, new ChatTypeEntry(bedrockType, textDecoration)); } 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 22942457a..af7928477 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 @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.java; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundSystemChatPacket; import com.nukkitx.protocol.bedrock.packet.TextPacket; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.ChatTypeEntry; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -37,11 +38,15 @@ public class JavaSystemChatTranslator extends PacketTranslator<ClientboundSystem @Override public void translate(GeyserSession session, ClientboundSystemChatPacket packet) { + ChatTypeEntry chatTypeEntry = session.getChatTypes().get(packet.getTypeId()); + // This probably isn't proper but system chat won't care about the registry in 1.19.1 anyway + TextPacket.Type chatType = chatTypeEntry == null ? TextPacket.Type.RAW : chatTypeEntry.bedrockChatType(); + TextPacket textPacket = new TextPacket(); textPacket.setPlatformChatId(""); textPacket.setSourceName(""); textPacket.setXuid(session.getAuthData().xuid()); - textPacket.setType(session.getChatTypes().get(packet.getTypeId()).bedrockChatType()); + textPacket.setType(chatType); textPacket.setNeedsTranslation(false); textPacket.setMessage(MessageTranslator.convertMessage(packet.getContent(), session.locale())); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java index d82a20a27..3d44ce2fd 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java @@ -147,6 +147,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit soundPacket.setRelativeVolumeDisabled(false); session.sendUpstreamPacket(soundPacket); return; + case VILLAGER_MATE: case ANIMAL_EMIT_HEARTS: entityEventPacket.setType(EntityEventType.LOVE_PARTICLES); break; @@ -176,6 +177,18 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit case IRON_GOLEM_HOLD_POPPY: entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_OFFER); break; + case VILLAGER_ANGRY: + entityEventPacket.setType(EntityEventType.VILLAGER_ANGRY); + break; + case VILLAGER_HAPPY: + entityEventPacket.setType(EntityEventType.VILLAGER_HAPPY); + break; + case VILLAGER_SWEAT: + LevelEventPacket levelEventPacket = new LevelEventPacket(); + levelEventPacket.setType(LevelEventType.PARTICLE_SPLASH); + levelEventPacket.setPosition(entity.getPosition().up(entity.getDefinition().height())); + session.sendUpstreamPacket(levelEventPacket); + return; case IRON_GOLEM_EMPTY_HAND: entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_WITHDRAW); break; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java index 6afb0b3ef..523d0fdc4 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java @@ -35,6 +35,6 @@ public class JavaBlockChangedAckTranslator extends PacketTranslator<ClientboundB @Override public void translate(GeyserSession session, ClientboundBlockChangedAckPacket packet) { - // TODO + session.getWorldCache().endPredictionsUpTo(packet.getSequence()); } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java index 44ae7f425..783f4e824 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java @@ -38,6 +38,8 @@ public class JavaPlayerAbilitiesTranslator extends PacketTranslator<ClientboundP session.setCanFly(packet.isCanFly()); session.setFlying(packet.isFlying()); session.setInstabuild(packet.isCreative()); + session.setFlySpeed(packet.getFlySpeed()); + session.setWalkSpeed(packet.getWalkSpeed()); session.sendAdventureSettings(); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerCombatKillTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerCombatKillTranslator.java new file mode 100644 index 000000000..89be26e4a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerCombatKillTranslator.java @@ -0,0 +1,50 @@ +/* + * 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.translator.protocol.java.entity.player; + +import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerCombatKillPacket; +import com.nukkitx.protocol.bedrock.packet.DeathInfoPacket; +import net.kyori.adventure.text.Component; +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 org.geysermc.geyser.translator.text.MessageTranslator; + +@Translator(packet = ClientboundPlayerCombatKillPacket.class) +public class JavaPlayerCombatKillTranslator extends PacketTranslator<ClientboundPlayerCombatKillPacket> { + + @Override + public void translate(GeyserSession session, ClientboundPlayerCombatKillPacket packet) { + if (packet.getPlayerId() == session.getPlayerEntity().getEntityId() && GameProtocol.supports1_19_10(session)) { + Component deathMessage = packet.getMessage(); + // TODO - could inject score in, but as of 1.19.10 newlines don't center and start at the left of the first text + DeathInfoPacket deathInfoPacket = new DeathInfoPacket(); + deathInfoPacket.setCauseAttackName(MessageTranslator.convertMessage(deathMessage, session.locale())); + session.sendUpstreamPacket(deathInfoPacket); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java index f5d21ecc9..2d2d7279f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java @@ -31,6 +31,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.Serverb import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData; +import com.nukkitx.protocol.bedrock.packet.ChunkRadiusUpdatedPacket; import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; import com.nukkitx.protocol.bedrock.packet.RespawnPacket; import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket; @@ -84,6 +85,15 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl acceptTeleport(session, packet.getX(), packet.getY(), packet.getZ(), packet.getYaw(), packet.getPitch(), packet.getTeleportId()); + if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) { + // See DimensionUtils for an explanation + ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket(); + chunkRadiusUpdatedPacket.setRadius(session.getServerRenderDistance()); + session.sendUpstreamPacket(chunkRadiusUpdatedPacket); + + session.setLastChunkPosition(null); + } + ChunkUtils.updateChunkPosition(session, pos.toInt()); if (session.getGeyser().getConfig().isDebugMode()) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java index 36307e7bd..aef8cf8b2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java @@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket; import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; @@ -66,27 +67,41 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound if (inventory == null) return; - // Intentional behavior here below the cursor; Minecraft 1.18.1 also does this. - int stateId = packet.getStateId(); - session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId()); - inventory.setStateId(stateId); - InventoryTranslator translator = session.getInventoryTranslator(); if (translator != null) { if (session.getCraftingGridFuture() != null) { session.getCraftingGridFuture().cancel(false); } - updateCraftingGrid(session, packet.getSlot(), packet.getItem(), inventory, translator); + + int slot = packet.getSlot(); + if (slot >= inventory.getSize()) { + GeyserImpl geyser = session.getGeyser(); + geyser.getLogger().warning("ClientboundContainerSetSlotPacket sent to " + session.name() + + " that exceeds inventory size!"); + if (geyser.getConfig().isDebugMode()) { + geyser.getLogger().debug(packet); + geyser.getLogger().debug(inventory); + } + // 1.19.0 behavior: the state ID will not be set due to exception + return; + } + + updateCraftingGrid(session, slot, packet.getItem(), inventory, translator); GeyserItemStack newItem = GeyserItemStack.from(packet.getItem()); if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) { // In rare cases, the window ID can still be 0 but Java treats it as valid - session.getPlayerInventory().setItem(packet.getSlot(), newItem, session); - InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), packet.getSlot()); + session.getPlayerInventory().setItem(slot, newItem, session); + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), slot); } else { - inventory.setItem(packet.getSlot(), newItem, session); - translator.updateSlot(session, inventory, packet.getSlot()); + inventory.setItem(slot, newItem, session); + translator.updateSlot(session, inventory, slot); } + + // Intentional behavior here below the cursor; Minecraft 1.18.1 also does this. + int stateId = packet.getStateId(); + session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId()); + inventory.setStateId(stateId); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java new file mode 100644 index 000000000..f222682ae --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java @@ -0,0 +1,61 @@ +/* + * 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.translator.protocol.java.level; + +import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundCooldownPacket; +import com.nukkitx.protocol.bedrock.packet.PlayerStartItemCooldownPacket; +import org.geysermc.geyser.inventory.item.StoredItemMappings; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; + +@Translator(packet = ClientboundCooldownPacket.class) +public class JavaCooldownTranslator extends PacketTranslator<ClientboundCooldownPacket> { + + @Override + public void translate(GeyserSession session, ClientboundCooldownPacket packet) { + StoredItemMappings itemMappings = session.getItemMappings().getStoredItems(); + + int itemId = packet.getItemId(); + // Not every item, as of 1.19, appears to be server-driven. Just these two. + // Use a map here if it gets too big. + String cooldownCategory; + if (itemId == itemMappings.goatHorn()) { + cooldownCategory = "goat_horn"; + } else if (itemId == itemMappings.shield().getJavaId()) { + cooldownCategory = "shield"; + } else { + cooldownCategory = null; + } + + if (cooldownCategory != null) { + PlayerStartItemCooldownPacket bedrockPacket = new PlayerStartItemCooldownPacket(); + bedrockPacket.setItemCategory(cooldownCategory); + bedrockPacket.setCooldownDuration(packet.getCooldownTicks()); + session.sendUpstreamPacket(bedrockPacket); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java index 5c7b2dd95..c4fabbd3d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java @@ -142,7 +142,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo } // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock - if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) { + if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) { bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session, Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)), javaId @@ -183,7 +183,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo } // Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock - if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) { + if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) { bedrockOnlyBlockEntityIds.set(i); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java index 2cbcd329a..62378b3d9 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java @@ -38,7 +38,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr @Override public void translate(GeyserSession session, Vector3f position, String identifier) { - if (session.getBucketScheduledFuture() == null) { + if (!session.isPlacedBucket()) { return; // No bucket was really interacted with } GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(); @@ -71,6 +71,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr case "minecraft:salmon_bucket": case "minecraft:pufferfish_bucket": case "minecraft:tropical_fish_bucket": + case "minecraft:tadpole_bucket": soundEvent = SoundEvent.BUCKET_EMPTY_FISH; break; case "minecraft:water_bucket": @@ -83,7 +84,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr if (soundEvent != null) { soundEventPacket.setSound(soundEvent); session.sendUpstreamPacket(soundEventPacket); - session.setBucketScheduledFuture(null); + session.setPlacedBucket(false); } } } diff --git a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java index 7059c9a8b..c0d484919 100644 --- a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java @@ -36,6 +36,8 @@ import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.collision.BlockCollision; +import javax.annotation.Nullable; + public final class BlockUtils { private static boolean correctTool(GeyserSession session, BlockMapping blockMapping, String itemToolType) { @@ -101,7 +103,7 @@ public final class BlockUtils { // https://minecraft.gamepedia.com/Breaking private static double calculateBreakTime(double blockHardness, String toolTier, boolean canHarvestWithHand, boolean correctTool, boolean canTierMineBlock, String toolType, boolean isShearsEffective, int toolEfficiencyLevel, int hasteLevel, int miningFatigueLevel, - boolean insideOfWaterWithoutAquaAffinity, boolean outOfWaterButNotOnGround, boolean insideWaterAndNotOnGround) { + boolean insideOfWaterWithoutAquaAffinity, boolean onGround) { double baseTime = (((correctTool && canTierMineBlock) || canHarvestWithHand) ? 1.5 : 5.0) * blockHardness; double speed = 1.0 / baseTime; @@ -129,12 +131,11 @@ public final class BlockUtils { } if (insideOfWaterWithoutAquaAffinity) speed *= 0.2; - if (outOfWaterButNotOnGround) speed *= 0.2; - if (insideWaterAndNotOnGround) speed *= 0.2; + if (!onGround) speed *= 0.2; return 1.0 / speed; } - public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, CompoundTag nbtData, boolean isSessionPlayer) { + public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, @Nullable CompoundTag nbtData, boolean isSessionPlayer) { boolean isShearsEffective = session.getTagCache().isShearsEffective(blockMapping); //TODO called twice boolean canHarvestWithHand = blockMapping.isCanBreakWithHand(); String toolType = ""; @@ -154,36 +155,28 @@ public final class BlockUtils { if (!isSessionPlayer) { // Another entity is currently mining; we have all the information we know return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective, - toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false, - false, false); + toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false, true); } hasteLevel = Math.max(session.getEffectCache().getHaste(), session.getEffectCache().getConduitPower()); miningFatigueLevel = session.getEffectCache().getMiningFatigue(); - boolean isInWater = session.getCollisionManager().isPlayerInWater(); - - boolean insideOfWaterWithoutAquaAffinity = isInWater && + boolean waterInEyes = session.getCollisionManager().isWaterInEyes(); + boolean insideOfWaterWithoutAquaAffinity = waterInEyes && ItemUtils.getEnchantmentLevel(session.getPlayerInventory().getItem(5).getNbt(), "minecraft:aqua_affinity") < 1; - boolean outOfWaterButNotOnGround = (!isInWater) && (!session.getPlayerEntity().isOnGround()); - boolean insideWaterNotOnGround = isInWater && !session.getPlayerEntity().isOnGround(); return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective, - toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, - outOfWaterButNotOnGround, insideWaterNotOnGround); + toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, session.getPlayerEntity().isOnGround()); } public static double getSessionBreakTime(GeyserSession session, BlockMapping blockMapping) { PlayerInventory inventory = session.getPlayerInventory(); GeyserItemStack item = inventory.getItemInHand(); - ItemMapping mapping; - CompoundTag nbtData; + ItemMapping mapping = ItemMapping.AIR; + CompoundTag nbtData = null; if (item != null) { mapping = item.getMapping(session); nbtData = item.getNbt(); - } else { - mapping = ItemMapping.AIR; - nbtData = new CompoundTag(""); } return getBreakTime(session, blockMapping, mapping, nbtData, true); } diff --git a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java index d1ee9f165..b87f82f90 100644 --- a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java @@ -123,13 +123,21 @@ public class ChunkUtils { * @param position the position of the block */ public static void updateBlock(GeyserSession session, int blockState, Vector3i position) { + updateBlockClientSide(session, blockState, position); + session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState); + session.getWorldCache().updateServerCorrectBlockState(position); + } + + /** + * Updates a block, but client-side only. + */ + public static void updateBlockClientSide(GeyserSession session, int blockState, Vector3i position) { // Checks for item frames so they aren't tripped up and removed ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position); if (itemFrameEntity != null) { if (blockState == JAVA_AIR_ID) { // Item frame is still present and no block overrides that; refresh it itemFrameEntity.updateBlock(true); - // Still update the chunk cache with the new block - session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState); + // Still update the chunk cache with the new block if updateBlock is called return; } // Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now @@ -175,7 +183,6 @@ public class ChunkUtils { break; //No block will be a part of two classes } } - session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState); } public static void sendEmptyChunk(GeyserSession session, int chunkX, int chunkZ, boolean forceUpdate) { diff --git a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java index fbc891131..7e5d65a97 100644 --- a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.util; import com.github.steveice10.mc.protocol.data.game.entity.Effect; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.packet.ChangeDimensionPacket; +import com.nukkitx.protocol.bedrock.packet.ChunkRadiusUpdatedPacket; import com.nukkitx.protocol.bedrock.packet.MobEffectPacket; import com.nukkitx.protocol.bedrock.packet.StopSoundPacket; import org.geysermc.geyser.entity.type.Entity; @@ -69,6 +70,22 @@ public class DimensionUtils { session.getPistonCache().clear(); session.getSkullCache().clear(); + if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) { + // The server-sided view distance wasn't a thing until Minecraft Java 1.14 + // So ViaVersion compensates by sending a "view distance" of 64 + // That's fine, except when the actual view distance sent from the server is five chunks + // The client locks up when switching dimensions, expecting more chunks than it's getting + // To solve this, we cap at 32 unless we know that the render distance actually exceeds 32 + // 47 is the Bedrock equivalent of 32 + // Also, as of 1.19: PS4 crashes with a ChunkRadiusUpdatedPacket too large + session.getGeyser().getLogger().debug("Applying dimension switching workaround for Bedrock render distance of " + + session.getServerRenderDistance()); + ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket(); + chunkRadiusUpdatedPacket.setRadius(47); + session.sendUpstreamPacket(chunkRadiusUpdatedPacket); + // Will be re-adjusted on spawn + } + Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0); ChangeDimensionPacket changeDimensionPacket = new ChangeDimensionPacket(); diff --git a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java index 43ccff5e0..d128989a8 100644 --- a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java @@ -187,6 +187,15 @@ public final class EntityUtils { case MINECART, HOPPER_MINECART, TNT_MINECART, CHEST_MINECART, FURNACE_MINECART, SPAWNER_MINECART, COMMAND_BLOCK_MINECART, BOAT, CHEST_BOAT -> yOffset -= mount.getDefinition().height() * 0.5f; } + if (passenger.getDefinition().entityType() == EntityType.FALLING_BLOCK) { + yOffset += 0.5f; + } + if (mount.getDefinition().entityType() == EntityType.ARMOR_STAND) { + ArmorStandEntity armorStand = (ArmorStandEntity) mount; + if (armorStand.isPositionRequiresOffset()) { + yOffset -= EntityDefinitions.ARMOR_STAND.height() * (armorStand.isSmall() ? 0.55d : 1d); + } + } Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset); passenger.setRiderSeatPosition(offset); } diff --git a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java index abc95839d..c8d6e42d7 100644 --- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java @@ -314,7 +314,14 @@ public class LoginEncryptionUtils { .label("geyser.auth.login.form.details.desc") .input("geyser.auth.login.form.details.email", "account@geysermc.org", "") .input("geyser.auth.login.form.details.pass", "123456", "") - .closedOrInvalidResultHandler(() -> buildAndShowLoginDetailsWindow(session)) + .invalidResultHandler(() -> buildAndShowLoginDetailsWindow(session)) + .closedResultHandler(() -> { + if (session.isMicrosoftAccount()) { + buildAndShowMicrosoftAuthenticationWindow(session); + } else { + buildAndShowLoginWindow(session); + } + }) .validResultHandler((response) -> session.authenticate(response.next(), response.next()))); } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0bc694b43..dd08f3922 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,7 +8,12 @@ dependencyResolutionManagement { mavenContent { releasesOnly() } } maven("https://repo.opencollab.dev/maven-snapshots") { - mavenContent { snapshotsOnly() } + mavenContent { + // This has the unintended side effect of not allowing snapshot version pinning. + // Likely a bug in Gradle's implementation of snapshot pinning + // See https://github.com/gradle/gradle/pull/406 + snapshotsOnly() + } } // Paper, Velocity