diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts index 84061650d..0216a43dd 100644 --- a/bootstrap/bungeecord/build.gradle.kts +++ b/bootstrap/bungeecord/build.gradle.kts @@ -8,7 +8,10 @@ dependencies { implementation(libs.cloud.bungee) implementation(libs.adventure.text.serializer.bungeecord) - compileOnlyApi(libs.bungeecord.proxy) + compileOnlyApi(libs.bungeecord.proxy) { + isTransitive = false + } + compileOnlyApi(libs.bungeecord.api) } platformRelocate("net.md_5.bungee.jni") diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java index ec34766dc..6948bd1e1 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java @@ -26,17 +26,21 @@ package org.geysermc.geyser.platform.mod; import net.minecraft.server.level.ServerPlayer; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.Permissions; import org.geysermc.geyser.platform.mod.command.ModCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; public final class GeyserModUpdateListener { public static void onPlayReady(ServerPlayer player) { - // Should be creating this in the supplier, but we need it for the permission check. - // Not a big deal currently because ModCommandSource doesn't load locale, so don't need to try to wait for it. - ModCommandSource source = new ModCommandSource(player.createCommandSourceStack()); - if (source.hasPermission(Permissions.CHECK_UPDATE)) { - VersionCheckUtils.checkForGeyserUpdate(() -> source); + // We could just not register the listener, but, this allows config reloading + if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { + // Should be creating this in the supplier, but we need it for the permission check. + // Not a big deal currently because ModCommandSource doesn't load locale, so don't need to try to wait for it. + ModCommandSource source = new ModCommandSource(player.createCommandSourceStack()); + if (source.hasPermission(Permissions.CHECK_UPDATE)) { + VersionCheckUtils.checkForGeyserUpdate(() -> source); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 72aa9ba4c..4908c0614 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -149,7 +149,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { */ public static final String OAUTH_CLIENT_ID = "204cefd1-4818-4de1-b98d-513fae875d88"; - private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"; + private static final Pattern IP_REGEX = Pattern.compile("\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"); private final SessionManager sessionManager = new SessionManager(); @@ -410,7 +410,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { String remoteAddress = config.getRemote().address(); // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. - if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { + if (!IP_REGEX.matcher(remoteAddress).matches() && !remoteAddress.equalsIgnoreCase("localhost")) { String[] record = WebUtils.findSrvRecord(this, remoteAddress); if (record != null) { int remotePort = Integer.parseInt(record[2]); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java index 309fa9cf1..b294c4766 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java @@ -208,7 +208,7 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle { @Override public boolean isClientControlled() { - if (!hasBodyArmor() || getFlag(EntityFlag.NO_AI) || staysStill) { + if (!hasBodyArmor() || staysStill) { return false; } diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java index 10cbcf556..2bfa6cce6 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java @@ -63,7 +63,7 @@ import java.util.regex.Pattern; @RequiredArgsConstructor public class GeyserExtensionLoader extends ExtensionLoader { - private static final Pattern[] EXTENSION_FILTERS = new Pattern[] { Pattern.compile("^.+\\.jar$") }; + private static final Pattern EXTENSION_FILTER = Pattern.compile("^.+\\.jar$"); private final Object2ObjectMap> classes = new Object2ObjectOpenHashMap<>(); private final Map classLoaders = new HashMap<>(); @@ -133,8 +133,8 @@ public class GeyserExtensionLoader extends ExtensionLoader { } } - public Pattern[] extensionFilters() { - return EXTENSION_FILTERS; + public Pattern extensionFilter() { + return EXTENSION_FILTER; } public Class classByName(final String name) throws ClassNotFoundException{ @@ -249,17 +249,15 @@ public class GeyserExtensionLoader extends ExtensionLoader { */ private void processExtensionsFolder(Path directory, ThrowingBiConsumer accept, BiConsumer reject) throws IOException { List extensionPaths = Files.list(directory).toList(); - Pattern[] extensionFilters = this.extensionFilters(); + Pattern extensionFilter = this.extensionFilter(); extensionPaths.forEach(path -> { if (Files.isDirectory(path)) { return; } // Only look at files that meet the extension filter - for (Pattern filter : extensionFilters) { - if (!filter.matcher(path.getFileName().toString()).matches()) { - return; - } + if (!extensionFilter.matcher(path.getFileName().toString()).matches()) { + return; } try { diff --git a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java index aac51a225..67ed59680 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java @@ -134,7 +134,7 @@ public class BlockInventoryHolder extends InventoryHolder { // and the bedrock block is vanilla BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getLastInteractionBlockPosition()); if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(state.javaId())) { - if (isValidBlock(state)) { + if (isValidBlock(session, session.getLastInteractionBlockPosition(), state)) { // We can safely use this block container.setHolderPosition(session.getLastInteractionBlockPosition()); container.setUsingRealBlock(true, state.block()); @@ -161,7 +161,7 @@ public class BlockInventoryHolder extends InventoryHolder { /** * @return true if this Java block ID can be used for player inventory. */ - protected boolean isValidBlock(BlockState blockState) { + protected boolean isValidBlock(GeyserSession session, Vector3i position, BlockState blockState) { return this.validBlocks.contains(blockState.block()); } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index b28b4ac3b..7c614255a 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -266,6 +266,7 @@ public class Item { if (bedrockEnchantment == null) { String enchantmentTranslation = MinecraftLocale.getLocaleString(enchantment.description(), session.locale()); addJavaOnlyEnchantment(session, builder, enchantmentTranslation, level); + builder.addEnchantmentGlint(); return null; } 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 e2b65eb0c..0997f72e2 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -32,7 +32,7 @@ import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818; import org.cloudburstmc.protocol.bedrock.codec.v819.Bedrock_v819; import org.cloudburstmc.protocol.bedrock.codec.v827.Bedrock_v827; -import org.cloudburstmc.protocol.bedrock.codec.v843.Bedrock_v843; +import org.cloudburstmc.protocol.bedrock.codec.v844.Bedrock_v844; import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; import org.geysermc.geyser.api.util.MinecraftVersion; import org.geysermc.geyser.impl.MinecraftVersionImpl; @@ -86,7 +86,7 @@ public final class GameProtocol { register(Bedrock_v818.CODEC, "1.21.90", "1.21.91", "1.21.92"); register(Bedrock_v819.CODEC, "1.21.93", "1.21.94"); register(Bedrock_v827.CODEC, "1.21.100", "1.21.101"); - register(Bedrock_v843.CODEC, "1.21.110"); + register(Bedrock_v844.CODEC, "1.21.110"); MinecraftVersion latestBedrock = SUPPORTED_BEDROCK_VERSIONS.get(SUPPORTED_BEDROCK_VERSIONS.size() - 1); DEFAULT_BEDROCK_VERSION = latestBedrock.versionString(); @@ -142,6 +142,10 @@ public final class GameProtocol { return session.protocolVersion() == Bedrock_v827.CODEC.getProtocolVersion(); } + public static boolean is1_21_110orHigher(GeyserSession session) { + return session.protocolVersion() >= Bedrock_v844.CODEC.getProtocolVersion(); + } + /** * Gets the supported Minecraft: Java Edition version names. * @@ -166,7 +170,7 @@ public final class GameProtocol { * @return the supported Minecraft: Java Edition version */ public static String getJavaMinecraftVersion() { - return "1.21.9-rc1"; // TODO change to 1.21.9 + return "1.21.9"; } /** 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 2bbbe0673..a8aa29106 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 @@ -46,7 +46,7 @@ import org.cloudburstmc.nbt.NbtUtils; import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818; import org.cloudburstmc.protocol.bedrock.codec.v819.Bedrock_v819; import org.cloudburstmc.protocol.bedrock.codec.v827.Bedrock_v827; -import org.cloudburstmc.protocol.bedrock.codec.v843.Bedrock_v843; +import org.cloudburstmc.protocol.bedrock.codec.v844.Bedrock_v844; import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.geysermc.geyser.GeyserImpl; @@ -60,7 +60,7 @@ import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.block.type.FlowerPotBlock; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.populator.conversion.Conversion827_819; -import org.geysermc.geyser.registry.populator.conversion.Conversion843_827; +import org.geysermc.geyser.registry.populator.conversion.Conversion844_827; import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.GeyserBedrockBlock; @@ -122,8 +122,8 @@ public final class BlockRegistryPopulator { var blockMappers = ImmutableMap., Remapper>builder() .put(ObjectIntPair.of("1_21_90", Bedrock_v818.CODEC.getProtocolVersion()), Conversion827_819::remapBlock) .put(ObjectIntPair.of("1_21_90", Bedrock_v819.CODEC.getProtocolVersion()), Conversion827_819::remapBlock) - .put(ObjectIntPair.of("1_21_100", Bedrock_v827.CODEC.getProtocolVersion()), Conversion843_827::remapBlock) - .put(ObjectIntPair.of("1_21_110", Bedrock_v843.CODEC.getProtocolVersion()), tag -> tag) + .put(ObjectIntPair.of("1_21_100", Bedrock_v827.CODEC.getProtocolVersion()), Conversion844_827::remapBlock) + .put(ObjectIntPair.of("1_21_110", Bedrock_v844.CODEC.getProtocolVersion()), tag -> tag) .build(); // We can keep this strong as nothing should be garbage collected diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java index 170edec1e..a3f42fecb 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomSkullRegistryPopulator.java @@ -49,9 +49,12 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.function.Function; +import java.util.regex.Pattern; public class CustomSkullRegistryPopulator { + private static final Pattern SKULL_HASH_PATTERN = Pattern.compile("^[a-fA-F0-9]{64}$"); + public static void populate() { SkullResourcePackManager.SKULL_SKINS.clear(); // Remove skins after reloading BlockRegistries.CUSTOM_SKULLS.set(Object2ObjectMaps.emptyMap()); @@ -118,7 +121,7 @@ public class CustomSkullRegistryPopulator { }); skinHashes.forEach((skinHash) -> { - if (!skinHash.matches("^[a-fA-F0-9]+$")) { + if (!SKULL_HASH_PATTERN.matcher(skinHash).matches()) { GeyserImpl.getInstance().getLogger().error("Skin hash " + skinHash + " does not match required format ^[a-fA-F0-9]{64}$ and will not be added as a custom block."); return; } 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 5bde81d6e..f90673389 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 @@ -48,7 +48,7 @@ import org.cloudburstmc.nbt.NbtUtils; import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818; import org.cloudburstmc.protocol.bedrock.codec.v819.Bedrock_v819; import org.cloudburstmc.protocol.bedrock.codec.v827.Bedrock_v827; -import org.cloudburstmc.protocol.bedrock.codec.v843.Bedrock_v843; +import org.cloudburstmc.protocol.bedrock.codec.v844.Bedrock_v844; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.SimpleItemDefinition; @@ -73,7 +73,7 @@ import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.level.block.property.Properties; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; -import org.geysermc.geyser.registry.populator.conversion.Conversion843_827; +import org.geysermc.geyser.registry.populator.conversion.Conversion844_827; import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.GeyserBedrockBlock; import org.geysermc.geyser.registry.type.GeyserMappingItem; @@ -192,10 +192,10 @@ public class ItemRegistryPopulator { eightOneEightFallbacks.put(Items.MUSIC_DISC_LAVA_CHICKEN, Items.MUSIC_DISC_CHIRP); List paletteVersions = new ArrayList<>(4); - paletteVersions.add(new PaletteVersion("1_21_90", Bedrock_v818.CODEC.getProtocolVersion(), eightOneEightFallbacks, Conversion843_827::remapItem)); - paletteVersions.add(new PaletteVersion("1_21_93", Bedrock_v819.CODEC.getProtocolVersion(), eightOneNineFallbacks, Conversion843_827::remapItem)); - paletteVersions.add(new PaletteVersion("1_21_100", Bedrock_v827.CODEC.getProtocolVersion(), eightTwoSevenFallbacks, Conversion843_827::remapItem)); - paletteVersions.add(new PaletteVersion("1_21_110", Bedrock_v843.CODEC.getProtocolVersion())); + paletteVersions.add(new PaletteVersion("1_21_90", Bedrock_v818.CODEC.getProtocolVersion(), eightOneEightFallbacks, Conversion844_827::remapItem)); + paletteVersions.add(new PaletteVersion("1_21_93", Bedrock_v819.CODEC.getProtocolVersion(), eightOneNineFallbacks, Conversion844_827::remapItem)); + paletteVersions.add(new PaletteVersion("1_21_100", Bedrock_v827.CODEC.getProtocolVersion(), eightTwoSevenFallbacks, Conversion844_827::remapItem)); + paletteVersions.add(new PaletteVersion("1_21_110", Bedrock_v844.CODEC.getProtocolVersion())); GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap(); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/TagRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/TagRegistryPopulator.java index e8f15c180..a27a83176 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/TagRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/TagRegistryPopulator.java @@ -36,7 +36,7 @@ import it.unimi.dsi.fastutil.objects.ObjectIntPair; import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818; import org.cloudburstmc.protocol.bedrock.codec.v819.Bedrock_v819; import org.cloudburstmc.protocol.bedrock.codec.v827.Bedrock_v827; -import org.cloudburstmc.protocol.bedrock.codec.v843.Bedrock_v843; +import org.cloudburstmc.protocol.bedrock.codec.v844.Bedrock_v844; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.item.type.Item; @@ -70,10 +70,10 @@ public final class TagRegistryPopulator { List> paletteVersions = List.of( ObjectIntPair.of("1_21_90", Bedrock_v818.CODEC.getProtocolVersion()), - // Not a typo, they're the same file + // Not a typo, it's the same file ObjectIntPair.of("1_21_90", Bedrock_v819.CODEC.getProtocolVersion()), ObjectIntPair.of("1_21_100", Bedrock_v827.CODEC.getProtocolVersion()), - ObjectIntPair.of("1_21_110", Bedrock_v843.CODEC.getProtocolVersion()) + ObjectIntPair.of("1_21_110", Bedrock_v844.CODEC.getProtocolVersion()) ); Type type = new TypeToken>>() {}.getType(); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion827_819.java b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion827_819.java index 41ee456fe..c04e70615 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion827_819.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion827_819.java @@ -30,7 +30,7 @@ import org.cloudburstmc.nbt.NbtMap; public class Conversion827_819 { public static NbtMap remapBlock(NbtMap nbtMap) { - nbtMap = Conversion843_827.remapBlock(nbtMap); + nbtMap = Conversion844_827.remapBlock(nbtMap); final String name = nbtMap.getString("name"); if (name.endsWith("copper_chest")) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion843_827.java b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion844_827.java similarity index 98% rename from core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion843_827.java rename to core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion844_827.java index d3ef9a608..511d27407 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion843_827.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion844_827.java @@ -31,7 +31,7 @@ import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.type.GeyserMappingItem; -public class Conversion843_827 { +public class Conversion844_827 { public static NbtMap remapBlock(NbtMap nbtMap) { final String name = nbtMap.getString("name"); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java b/core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java index 3eca7d7fe..041f76c03 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java @@ -29,11 +29,13 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import it.unimi.dsi.fastutil.Pair; import lombok.Getter; +import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.PlayerActionType; import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData; import org.cloudburstmc.protocol.bedrock.data.PlayerBlockActionData; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; @@ -83,6 +85,7 @@ public class BlockBreakHandler { * The position of the current block being broken. * Null indicates no block breaking in progress. */ + @Getter protected @Nullable Vector3i currentBlockPos = null; /** @@ -92,22 +95,39 @@ public class BlockBreakHandler { protected @Nullable BlockState currentBlockState = null; /** - * The Bedrock client tick in which block breaking of the current block began. + * Indicates that we should re-check the current block state for changes + */ + @Setter + protected @Nullable Integer updatedServerBlockStateId; + + /** + * Whether we must break the block ourselves. * Only set when keeping track of custom blocks / custom items breaking blocks. */ - protected long blockStartBreakTime = 0; + protected boolean serverSideBlockBreaking = false; + + /** + * The current block breaking progress + */ + protected float currentProgress = 0.0F; /** * The last known face of the block the client was breaking. * Only set when keeping track of custom blocks / custom items breaking blocks. */ - protected Direction lastBlockBreakFace = null; + protected Direction currentBlockFace = null; /** - * The last block position that was instantly broken. + * The last item used to break blocks. + * Used to track whether block breaking should be re-started as the item changed + */ + protected GeyserItemStack currentItemStack = null; + + /** + * The last block position that was broken. * Used to ignore subsequent block actions from the Bedrock client. */ - protected Vector3i lastInstaMinedPosition = null; + protected Vector3i lastMinedPosition = null; /** * Caches all blocks we had to restore e.g. due to out-of-range or being unable to mine @@ -148,18 +168,19 @@ public class BlockBreakHandler { restoredBlocks.clear(); this.itemFramePos = null; } else { - tick(); + tick(packet.getTick()); } } - protected void tick() { + protected void tick(long tick) { // We need to manually check if a block should be destroyed, and send the client progress updates, when mining a custom block, or with a custom item // This is because, in CustomItemRegistryPopulator#computeToolProperties, we set a block break speed of 0, - // meaning the client will only ever send START_BREAK for breaking blocks, and nothing else + // meaning the client will only ever send START_BREAK for breaking blocks, and nothing else (as long as no efficiency is applied, lol) + // We also want to tick destroying to ensure that the currently held item did not change // Check lastBlockBreakFace, currentBlockPos and currentBlockState, just in case - if (blockStartBreakTime != 0 && lastBlockBreakFace != null && currentBlockPos != null && currentBlockState != null) { - handleContinueDestroy(currentBlockPos, currentBlockState, lastBlockBreakFace, session.getClientTicks()); + if (currentBlockFace != null && currentBlockPos != null && currentBlockState != null) { + handleContinueDestroy(currentBlockPos, getCurrentBlockState(currentBlockPos), currentBlockFace, false, false, session.getClientTicks()); } } @@ -170,51 +191,66 @@ public class BlockBreakHandler { // Worth noting: the bedrock client, as of version 1.21.101, sends weird values for the face, outside the [0;6] range, when sending ABORT_BREAK // Not sure why, but, blockFace isn't used for ABORT_BREAK, so it's fine // This is why blockFace is individually turned into a Direction in each of the switch statements, except for the ABORT_BREAK one - switch (actionData.getAction()) { case DROP_ITEM -> { ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, position, Direction.getUntrusted(actionData, PlayerBlockActionData::getFace).mcpl(), 0); session.sendDownstreamGamePacket(dropItemPacket); } - // Must do this ugly as it can also be called from the block_continue_destroy case :( - case START_BREAK -> preStartBreakHandle(position, Direction.getUntrusted(actionData, PlayerBlockActionData::getFace), packet.getTick()); - case BLOCK_CONTINUE_DESTROY -> { - if (testForItemFrameEntity(position) || testForLastInstaBreakPosOrReset(position) || abortDueToBlockRestoring(position)) { + case START_BREAK -> { + // New block being broken -> ignore previously mined position since that's no longer relevant + this.lastMinedPosition = null; + + if (testForItemFrameEntity(position) || abortDueToBlockRestoring(position)) { continue; } - Direction blockFace = Direction.getUntrusted(actionData, PlayerBlockActionData::getFace); - // Position mismatch == we break a new block! Bedrock won't send START_BREAK when continuously mining - // That applies in creative mode too! (last test in 1.21.100) - if (!Objects.equals(position, currentBlockPos) || currentBlockState == null) { - if (currentBlockPos != null) { - handleAbortBreaking(currentBlockPos); - } - preStartBreakHandle(position, blockFace, packet.getTick()); + BlockState state = getCurrentBlockState(position); + if (!canBreak(position, state, actionData.getAction())) { + BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); + restoredBlocks.add(position); + continue; + } + + handleStartBreak(position, state, Direction.getUntrusted(actionData, PlayerBlockActionData::getFace), packet.getTick()); + } + case BLOCK_CONTINUE_DESTROY -> { + if (testForItemFrameEntity(position) || testForLastBreakPosOrReset(position) || abortDueToBlockRestoring(position)) { continue; } // The client loves to send this block action alongside BLOCK_PREDICT_DESTROY in the same packet; - // we can skip handling this action if the same position is updated again in the same tick - if (i < packet.getPlayerActions().size() - 1) { + // we can skip handling this action about the current position if the next action is also about it + if (Objects.equals(currentBlockPos, position) && i < packet.getPlayerActions().size() - 1) { PlayerBlockActionData nextAction = packet.getPlayerActions().get(i + 1); if (Objects.equals(nextAction.getBlockPosition(), position)) { continue; } } - BlockState state = session.getGeyser().getWorldManager().blockAt(session, position); - if (!canBreak(position, state)) { + BlockState state = getCurrentBlockState(position); + if (!canBreak(position, state, actionData.getAction())) { BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); restoredBlocks.add(position); + + // Also abort old / "current" block breaking, if there is one in progress + if (!Objects.equals(currentBlockPos, position)) { + handleAbortBreaking(position); + } continue; } - handleContinueDestroy(position, state, blockFace, packet.getTick()); + handleContinueDestroy(position, state, Direction.getUntrusted(actionData, PlayerBlockActionData::getFace), false, true, packet.getTick()); } case BLOCK_PREDICT_DESTROY -> { - if (testForItemFrameEntity(position) || testForLastInstaBreakPosOrReset(position)) { + if (testForItemFrameEntity(position)) { + continue; + } + + // At this point it's safe to assume that we won't get subsequent block actions on this position + // so reset it and return since we've already broken the block + if (Objects.equals(lastMinedPosition, position)) { + lastMinedPosition = null; continue; } @@ -225,12 +261,13 @@ public class BlockBreakHandler { continue; } - BlockState state = session.getGeyser().getWorldManager().blockAt(session, position); - boolean valid = currentBlockState != null && Objects.equals(position, currentBlockPos); - if (!canBreak(position, state) || !valid) { + BlockState state = getCurrentBlockState(position); + boolean valid = currentBlockPos != null && Objects.equals(position, currentBlockPos); + if (!canBreak(position, state, actionData.getAction()) || !valid) { if (!valid) { GeyserImpl.getInstance().getLogger().warning("Player %s tried to break block at %s (%s), without starting to destroy it!" - .formatted(session.bedrockUsername(), position, currentBlockState)); + .formatted(session.bedrockUsername(), position, currentBlockPos)); + handleAbortBreaking(currentBlockPos); } BlockUtils.stopBreakAndRestoreBlock(session, position, state); restoredBlocks.add(position); @@ -240,14 +277,6 @@ public class BlockBreakHandler { handlePredictDestroy(position, state, Direction.getUntrusted(actionData, PlayerBlockActionData::getFace), packet.getTick()); } case ABORT_BREAK -> { - // Abort break can also be sent after the block on that pos was broken..... - // At that point it's safe to assume that we won't get subsequent block actions on this position - // so reset it and return since there isn't anything to abort - if (Objects.equals(lastInstaMinedPosition, position)) { - lastInstaMinedPosition = null; - continue; - } - // Also handles item frame interactions in adventure mode if (testForItemFrameEntity(position)) { continue; @@ -265,28 +294,6 @@ public class BlockBreakHandler { } } - /** - * Called from either a START_BREAK or BLOCK_CONTINUE_DESTROY case, the latter - * if the client switches to a new block. This method then runs pre-break checks. - */ - private void preStartBreakHandle(Vector3i position, Direction blockFace, long tick) { - // New block being broken -> ignore previous insta-mine pos since that's no longer relevant - lastInstaMinedPosition = null; - - if (testForItemFrameEntity(position) || abortDueToBlockRestoring(position)) { - return; - } - - BlockState state = session.getGeyser().getWorldManager().blockAt(session, position); - if (!canBreak(position, state)) { - BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); - restoredBlocks.add(position); - return; - } - - handleStartBreak(position, state, blockFace, tick); - } - protected void handleStartBreak(@NonNull Vector3i position, @NonNull BlockState state, Direction blockFace, long tick) { GeyserItemStack item = session.getPlayerInventory().getItemInHand(); @@ -305,8 +312,8 @@ public class BlockBreakHandler { // insta-breaking should be treated differently; don't send STOP_BREAK for these if (session.isInstabuild() || breakProgress >= 1.0F) { // Avoid sending STOP_BREAK for instantly broken blocks - lastInstaMinedPosition = position; destroyBlock(state, position, blockFace, true); + this.lastMinedPosition = position; } else { // If the block is custom or the breaking item is custom, we must keep track of break time ourselves ItemMapping mapping = item.getMapping(session); @@ -314,11 +321,10 @@ public class BlockBreakHandler { CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(state.javaId()); SkullCache.Skull skull = session.getSkullCache().getSkulls().get(position); - this.blockStartBreakTime = 0; + this.serverSideBlockBreaking = false; if (BlockRegistries.NON_VANILLA_BLOCK_IDS.get().get(state.javaId()) || blockStateOverride != null || - customItem != null || (skull != null && skull.getBlockDefinition() != null)) { - this.blockStartBreakTime = tick; - this.lastBlockBreakFace = blockFace; + customItem != null || (skull != null && skull.getBlockDefinition() != null)) { + this.serverSideBlockBreaking = true; } LevelEventPacket startBreak = new LevelEventPacket(); @@ -329,42 +335,82 @@ public class BlockBreakHandler { BlockUtils.spawnBlockBreakParticles(session, blockFace, position, state); + this.currentBlockFace = blockFace; this.currentBlockPos = position; this.currentBlockState = state; + this.currentItemStack = item; + // The Java client calls MultiPlayerGameMode#startDestroyBlock which would set this to zero, + // but also #continueDestroyBlock in the same tick to advance the break progress. + this.currentProgress = breakProgress; session.sendDownstreamGamePacket(new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, position, blockFace.mcpl(), session.getWorldCache().nextPredictionSequence())); } } - protected void handleContinueDestroy(Vector3i position, BlockState state, Direction blockFace, long tick) { - BlockUtils.spawnBlockBreakParticles(session, blockFace, position, state); - double totalBreakTime = BlockUtils.reciprocal(calculateBreakProgress(state, position, session.getPlayerInventory().getItemInHand())); + protected void handleContinueDestroy(@NonNull Vector3i position, @NonNull BlockState state, @NonNull Direction blockFace, boolean bedrockDestroyed, boolean sendParticles, long tick) { + // Position mismatch == we break a new block! Bedrock won't send START_BREAK when continuously mining + // That applies in creative mode too! (last test in 1.21.100) + // Further: We should also "start" breaking te block anew if the held item changes. + // As of 1.21.100 it seems like this is in fact NOT done by BDS! + if (currentBlockState != null && Objects.equals(position, currentBlockPos) && sameItemStack()) { + this.currentBlockFace = blockFace; - if (blockStartBreakTime != 0) { - long ticksSinceStart = tick - blockStartBreakTime; - // We need to add a slight delay to the break time, otherwise the client breaks blocks too fast - if (ticksSinceStart >= (totalBreakTime += 2)) { - destroyBlock(state, position, blockFace, false); - return; + final float newProgress = calculateBreakProgress(state, position, session.getPlayerInventory().getItemInHand()); + this.currentProgress = this.currentProgress + newProgress; + double totalBreakTime = BlockUtils.reciprocal(newProgress); + + if (sendParticles || (serverSideBlockBreaking && currentProgress % 4 == 0)) { + BlockUtils.spawnBlockBreakParticles(session, blockFace, position, state); } - // Update in case it has changed - lastBlockBreakFace = blockFace; - } - // Update the break time in the event that player conditions changed (jumping, effects applied) - LevelEventPacket updateBreak = new LevelEventPacket(); - updateBreak.setType(LevelEvent.BLOCK_UPDATE_BREAK); - updateBreak.setPosition(position.toFloat()); - updateBreak.setData((int) (65535 / totalBreakTime)); - session.sendUpstreamPacket(updateBreak); + // let's be a bit lenient here; the Vanilla server is as well + if (mayBreak(currentProgress, bedrockDestroyed)) { + destroyBlock(state, position, blockFace, false); + if (!bedrockDestroyed) { + // Only store it if we need to ignore subsequent Bedrock block actions + this.lastMinedPosition = position; + } + return; + } else if (bedrockDestroyed) { + BlockUtils.restoreCorrectBlock(session, position, state); + } + + // Update the break time in the event that player conditions changed (jumping, effects applied) + LevelEventPacket updateBreak = new LevelEventPacket(); + updateBreak.setType(LevelEvent.BLOCK_UPDATE_BREAK); + updateBreak.setPosition(position.toFloat()); + updateBreak.setData((int) (65535 / totalBreakTime)); + session.sendUpstreamPacket(updateBreak); + } else { + // Don't store last mined position; we don't want to ignore any actions now that we switched! + this.lastMinedPosition = null; + // We have switched - either between blocks, or are between the stack we're using to break the block + if (currentBlockPos != null) { + LevelEventPacket updateBreak = new LevelEventPacket(); + updateBreak.setType(LevelEvent.BLOCK_UPDATE_BREAK); + updateBreak.setPosition(position.toFloat()); + updateBreak.setData(0); + session.sendUpstreamPacketImmediately(updateBreak); + + // Prevent ghost blocks when Bedrock thinks it destroyed a block and wants to "move on", + // while it wasn't actually destroyed on our end. + if (bedrockDestroyed) { + BlockUtils.restoreCorrectBlock(session, currentBlockPos, currentBlockState); + } + + handleAbortBreaking(currentBlockPos); + } + + handleStartBreak(position, state, blockFace, tick); + } } protected void handlePredictDestroy(Vector3i position, BlockState state, Direction blockFace, long tick) { - destroyBlock(state, position, blockFace, false); + handleContinueDestroy(position, state, blockFace, true, true, tick); } - protected void handleAbortBreaking(Vector3i position) { + private void handleAbortBreaking(Vector3i position) { // Bedrock edition "confirms" it stopped breaking blocks by sending an abort packet // We don't forward those as a Java client wouldn't send those either if (currentBlockPos != null) { @@ -374,6 +420,7 @@ public class BlockBreakHandler { } BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); + this.clearCurrentVariables(); } /** @@ -411,6 +458,12 @@ public class BlockBreakHandler { if (!restoredBlocks.isEmpty()) { BlockUtils.sendBedrockStopBlockBreak(session, position.toFloat()); restoredBlocks.add(position); + + if (currentBlockPos != null && !Objects.equals(position, currentBlockPos)) { + restoredBlocks.add(currentBlockPos); + handleAbortBreaking(currentBlockPos); + } + return true; } return false; @@ -421,7 +474,7 @@ public class BlockBreakHandler { * This includes world border, "hands busy" (boat steering), and GameMode checks. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") - protected boolean canBreak(Vector3i vector, BlockState state) { + protected boolean canBreak(Vector3i vector, BlockState state, PlayerActionType action) { if (session.isHandsBusy() || !session.getWorldBorder().isInsideBorderBoundaries()) { return false; } @@ -460,6 +513,11 @@ public class BlockBreakHandler { return !state.is(Blocks.AIR); } + protected boolean mayBreak(float progress, boolean bedrockDestroyed) { + // We're tolerant here to account for e.g. obsidian breaking speeds not matching 1:1 :( + return (serverSideBlockBreaking && progress >= 1.0F) || (bedrockDestroyed && progress >= 0.7F); + } + protected void destroyBlock(BlockState state, Vector3i vector, Direction direction, boolean instamine) { // Send java packet session.sendDownstreamGamePacket(new ServerboundPlayerActionPacket(instamine ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING, @@ -484,21 +542,56 @@ public class BlockBreakHandler { * This ensures that Geyser does not send a FINISH_DIGGING player action for instantly mined blocks, * or those mined while in creative mode. */ - protected boolean testForLastInstaBreakPosOrReset(Vector3i position) { - if (Objects.equals(lastInstaMinedPosition, position)) { + protected boolean testForLastBreakPosOrReset(Vector3i position) { + if (Objects.equals(lastMinedPosition, position)) { return true; } - lastInstaMinedPosition = null; + lastMinedPosition = null; return false; } + private boolean sameItemStack() { + if (currentItemStack == null) { + return false; + } + GeyserItemStack stack = session.getPlayerInventory().getItemInHand(); + if (currentItemStack.isEmpty() && stack.isEmpty()) { + return true; + } + if (currentItemStack.getJavaId() != stack.getJavaId()) { + return false; + } + + return Objects.equals(stack.getComponents(), currentItemStack.getComponents()); + } + + private @NonNull BlockState getCurrentBlockState(Vector3i position) { + if (Objects.equals(position, currentBlockPos)) { + if (updatedServerBlockStateId != null) { + BlockState updated = BlockState.of(updatedServerBlockStateId); + this.updatedServerBlockStateId = null; + return updated; + } + + if (currentBlockState != null) { + return currentBlockState; + } + } + + this.updatedServerBlockStateId = null; + return session.getGeyser().getWorldManager().blockAt(session, position); + } + /** * Resets variables after a block was broken. */ protected void clearCurrentVariables() { this.currentBlockPos = null; this.currentBlockState = null; - this.blockStartBreakTime = 0L; + this.currentBlockFace = null; + this.currentProgress = 0.0F; + this.currentItemStack = null; + this.updatedServerBlockStateId = null; } /** @@ -506,7 +599,7 @@ public class BlockBreakHandler { */ public void reset() { clearCurrentVariables(); - this.lastInstaMinedPosition = null; + this.lastMinedPosition = null; this.destructionStageCache.invalidateAll(); } 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 6108c6432..f06c45d5f 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 @@ -47,6 +47,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty; import java.util.Iterator; import java.util.Map; +import java.util.Objects; public final class WorldCache { private final GeyserSession session; @@ -179,6 +180,12 @@ public final class WorldCache { this.unverifiedPredictions.removeInt(position); } + // Hack to avoid looking up blockstates for the currently broken position each tick + Vector3i clientBreakPos = session.getBlockBreakHandler().getCurrentBlockPos(); + if (clientBreakPos != null && Objects.equals(clientBreakPos, position)) { + session.getBlockBreakHandler().setUpdatedServerBlockStateId(blockState); + } + ChunkUtils.updateBlock(session, blockState, position); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/ShulkerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/ShulkerInventoryTranslator.java index a8adde806..d230e1dc0 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/ShulkerInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/ShulkerInventoryTranslator.java @@ -53,7 +53,7 @@ public class ShulkerInventoryTranslator extends AbstractBlockInventoryTranslator private final BlockEntityTranslator shulkerBoxTranslator = Registries.BLOCK_ENTITIES.get(BlockEntityType.SHULKER_BOX); @Override - protected boolean isValidBlock(BlockState blockState) { + protected boolean isValidBlock(GeyserSession session, Vector3i position, BlockState blockState) { return blockState.block().javaIdentifier().value().contains("shulker_box"); // TODO ew } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java index 74b228e70..84194d4fa 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/chest/SingleChestInventoryTranslator.java @@ -25,8 +25,10 @@ package org.geysermc.geyser.translator.inventory.chest; +import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.Generic9X3Container; import org.geysermc.geyser.inventory.holder.BlockInventoryHolder; import org.geysermc.geyser.inventory.holder.InventoryHolder; @@ -34,6 +36,9 @@ import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.level.block.property.ChestType; import org.geysermc.geyser.level.block.property.Properties; import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.Axis; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; public class SingleChestInventoryTranslator extends ChestInventoryTranslator { @@ -44,14 +49,34 @@ public class SingleChestInventoryTranslator extends ChestInventoryTranslator