diff --git a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java index 8753287387e..fa3e6b6170a 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java @@ -42,7 +42,6 @@ import org.geysermc.geyser.level.block.type.SkullBlock; import org.geysermc.geyser.level.block.type.TrapDoorBlock; import org.geysermc.geyser.level.block.type.WallSkullBlock; -import org.geysermc.geyser.level.block.type.WaterBlock; import org.geysermc.geyser.level.physics.Axis; import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.level.physics.PistonBehavior; @@ -104,7 +103,7 @@ public final class Blocks { .intState(STAGE) .booleanState(WATERLOGGED))); public static final Block BEDROCK = register(new Block("bedrock", builder().destroyTime(-1.0f))); - public static final Block WATER = register(new WaterBlock("water", builder().destroyTime(100.0f).pushReaction(PistonBehavior.DESTROY) + public static final Block WATER = register(new Block("water", builder().destroyTime(100.0f).pushReaction(PistonBehavior.DESTROY) .intState(LEVEL))); public static final Block LAVA = register(new Block("lava", builder().destroyTime(100.0f).pushReaction(PistonBehavior.DESTROY) .intState(LEVEL))); diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java b/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java index 0863aa8138e..a619ec60e52 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/Block.java @@ -86,6 +86,8 @@ public Block(@Subst("empty") String javaIdentifier, Builder builder) { this.defaultState = setDefaultState(firstState); } + // JavaSectionBlocksUpdateTranslator only calls updateBlock for specialized blocks, + // make sure to also update JavaSectionBlocksUpdateTranslator for general changes. public void updateBlock(GeyserSession session, BlockState state, Vector3i position) { checkForEmptySkull(session, state, position); diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/WaterBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/WaterBlock.java deleted file mode 100644 index 9d2d2311629..00000000000 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/WaterBlock.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2024 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.level.block.type; - -public class WaterBlock extends Block { - public WaterBlock(String javaIdentifier, Builder builder) { - super(javaIdentifier, builder); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java index 464b993836b..c6e78a33bb9 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java @@ -27,8 +27,8 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import lombok.Getter; import lombok.Setter; -import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.chunk.GeyserChunk; import org.geysermc.geyser.registry.BlockRegistries; @@ -37,6 +37,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette; public class ChunkCache { + @Getter private final boolean cache; private final Long2ObjectMap chunks; @@ -68,64 +69,53 @@ private GeyserChunk getChunk(int chunkX, int chunkZ) { return chunks.getOrDefault(chunkPosition, null); } - public void updateBlock(int x, int y, int z, int block) { - if (!cache) { - return; - } - - GeyserChunk chunk = this.getChunk(x >> 4, z >> 4); + /** + * Doesn't check for cache enabled, so don't use this without checking that first! + */ + @Deprecated + public DataPalette getChunkSection(int chunkX, int chunkY, int chunkZ, boolean createIfAbsent) { + GeyserChunk chunk = this.getChunk(chunkX, chunkZ); if (chunk == null) { - return; + return null; } - if (y < minY || ((y - minY) >> 4) > chunk.sections().length - 1) { - // Y likely goes above or below the height limit of this world - return; + if (chunkY < getChunkMinY() || chunkY - getChunkMinY() > chunk.sections().length - 1) { + return null; } - boolean previouslyEmpty = false; - try { - DataPalette palette = chunk.sections()[(y - minY) >> 4]; - if (palette == null) { - previouslyEmpty = true; - if (block != Block.JAVA_AIR_ID) { - // A previously empty chunk, which is no longer empty as a block has been added to it - palette = DataPalette.createForBlockState(Block.JAVA_AIR_ID, BlockRegistries.BLOCK_STATES.get().size()); - chunk.sections()[(y - minY) >> 4] = palette; - } else { - // Nothing to update - return; - } - } - - palette.set(x & 0xF, y & 0xF, z & 0xF, block); - } catch (Throwable e) { - GeyserImpl.getInstance().getLogger().error("Failed to update block in chunk cache! ", e); - GeyserImpl.getInstance().getLogger().error("Info: newChunk=%s, block=%s, pos=%s,%s,%s".formatted(previouslyEmpty, block, x, y, z)); + DataPalette palette = chunk.sections()[chunkY - getChunkMinY()]; + if (createIfAbsent && palette == null) { + palette = DataPalette.createForBlockState(Block.JAVA_AIR_ID, BlockRegistries.BLOCK_STATES.get().size()); + chunk.sections()[chunkY - getChunkMinY()] = palette; } + + return palette; } - public int getBlockAt(int x, int y, int z) { + public void updateBlock(int x, int y, int z, int block) { if (!cache) { - return Block.JAVA_AIR_ID; + return; } - GeyserChunk column = this.getChunk(x >> 4, z >> 4); - if (column == null) { - return Block.JAVA_AIR_ID; + DataPalette palette = this.getChunkSection(x >> 4, y >> 4, z >> 4, true); + if (palette == null) { + return; } - if (y < minY || ((y - minY) >> 4) > column.sections().length - 1) { - // Y likely goes above or below the height limit of this world + palette.set(x & 0xF, y & 0xF, z & 0xF, block); + } + + public int getBlockAt(int x, int y, int z) { + if (!cache) { return Block.JAVA_AIR_ID; } - DataPalette chunk = column.sections()[(y - minY) >> 4]; - if (chunk != null) { - return chunk.get(x & 0xF, y & 0xF, z & 0xF); + DataPalette palette = this.getChunkSection(x >> 4, y >> 4, z >> 4, false); + if (palette == null) { + return Block.JAVA_AIR_ID; } - return Block.JAVA_AIR_ID; + return palette.get(x & 0xF, y & 0xF, z & 0xF); } public void removeChunk(int chunkX, int chunkZ) { 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 070ae624985..e3ff2722f7e 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 @@ -180,9 +180,7 @@ public void markPositionInSequence(Vector3i position) { } public void updateServerCorrectBlockState(Vector3i position, int blockState) { - if (!this.unverifiedPredictions.isEmpty()) { - this.unverifiedPredictions.removeInt(position); - } + this.unverifiedPredictions.removeInt(position); // Hack to avoid looking up blockstates for the currently broken position each tick Vector3i clientBreakPos = session.getBlockBreakHandler().getCurrentBlockPos(); @@ -193,6 +191,10 @@ public void updateServerCorrectBlockState(Vector3i position, int blockState) { ChunkUtils.updateBlock(session, blockState, position); } + public void removePrediction(Vector3i position) { + this.unverifiedPredictions.removeInt(position); + } + public void endPredictionsUpTo(int sequence) { if (this.unverifiedPredictions.isEmpty()) { return; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java index a52bb33b0ed..587b786f5de 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSectionBlocksUpdateTranslator.java @@ -25,19 +25,105 @@ package org.geysermc.geyser.translator.protocol.java.level; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType; +import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateSubChunkBlocksPacket; +import org.geysermc.geyser.entity.type.ItemFrameEntity; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette; import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockChangeEntry; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundSectionBlocksUpdatePacket; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import java.util.BitSet; +import java.util.Objects; + @Translator(packet = ClientboundSectionBlocksUpdatePacket.class) public class JavaSectionBlocksUpdateTranslator extends PacketTranslator { + private static final int FLAG_ALL = 1 << UpdateBlockPacket.Flag.NEIGHBORS.ordinal() | 1 << UpdateBlockPacket.Flag.NETWORK.ordinal(); + @Override public void translate(GeyserSession session, ClientboundSectionBlocksUpdatePacket packet) { + DataPalette palette = null; + if (session.getChunkCache().isCache()) { + palette = session.getChunkCache().getChunkSection(packet.getChunkX(), packet.getChunkY(), packet.getChunkZ(), true); + if (palette == null) { + return; + } + } + + Vector3i clientBreakPos = session.getBlockBreakHandler().getCurrentBlockPos(); + BitSet waterlogged = BlockRegistries.WATERLOGGED.get(); + + UpdateSubChunkBlocksPacket updateSubChunkBlocksPacket = new UpdateSubChunkBlocksPacket(); + for (BlockChangeEntry entry : packet.getEntries()) { - session.getWorldCache().updateServerCorrectBlockState(entry.getPosition(), entry.getBlock()); + session.getWorldCache().removePrediction(entry.getPosition()); + + // Hack to avoid looking up blockstates for the currently broken position each tick + if (clientBreakPos != null && Objects.equals(clientBreakPos, entry.getPosition())) { + session.getBlockBreakHandler().setUpdatedServerBlockStateId(entry.getBlock()); + } + + int oldBlock = palette != null + ? palette.get(entry.getPosition().getX() & 0xF, entry.getPosition().getY() & 0xF, entry.getPosition().getZ() & 0xF) + : session.getGeyser().getWorldManager().getBlockAt(session, entry.getPosition()); + if (entry.getBlock() == oldBlock) { + // Skip unchanged blocks which may occur with older versions of Minecraft + continue; + } + + if (palette != null) { + palette.set(entry.getPosition().getX() & 0xF, entry.getPosition().getY() & 0xF, entry.getPosition().getZ() & 0xF, entry.getBlock()); + } + + BlockState blockState = BlockState.of(entry.getBlock()); + if (blockState.is(Blocks.AIR)) { + ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, entry.getPosition()); + if (itemFrameEntity != null) { // Item frame is still present and no block overrides that; refresh it + itemFrameEntity.updateBlock(true); + continue; + } + } + + // Some block may have special handling, keep it that way + if (!(blockState.block().getClass().equals(Block.class))) { + blockState.block().updateBlock(session, blockState, entry.getPosition()); + continue; + } + + // Skull is gone + session.getSkullCache().removeSkull(entry.getPosition()); + + updateSubChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry( + entry.getPosition(), + session.getBlockMappings().getBedrockBlock(blockState), + FLAG_ALL, + -1, + MessageType.NONE + )); + + boolean isWaterlogged = waterlogged.get(entry.getBlock()); + if (waterlogged.get(oldBlock) != isWaterlogged) { + updateSubChunkBlocksPacket.getExtraBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry( + entry.getPosition(), + isWaterlogged ? session.getBlockMappings().getBedrockWater() : session.getBlockMappings().getBedrockAir(), + 0, + -1, + MessageType.NONE + )); + } + } + + if (!updateSubChunkBlocksPacket.getStandardBlocks().isEmpty()) { + session.sendUpstreamPacket(updateSubChunkBlocksPacket); } } }