Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,6 +37,7 @@
import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette;

public class ChunkCache {
@Getter
private final boolean cache;
private final Long2ObjectMap<GeyserChunk> chunks;

Expand Down Expand Up @@ -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
Comment thread
valaphee marked this conversation as resolved.
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;
}
Comment thread
valaphee marked this conversation as resolved.

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);
}
Comment thread
valaphee marked this conversation as resolved.

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClientboundSectionBlocksUpdatePacket> {

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) {
Comment thread
valaphee marked this conversation as resolved.
return;
}
Comment thread
valaphee marked this conversation as resolved.
Comment thread
valaphee marked this conversation as resolved.
}
Comment thread
valaphee marked this conversation as resolved.

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) {
Comment thread
valaphee marked this conversation as resolved.
// Skip unchanged blocks which may occur with older versions of Minecraft
continue;
}
Comment thread
valaphee marked this conversation as resolved.
Comment thread
valaphee marked this conversation as resolved.

if (palette != null) {
palette.set(entry.getPosition().getX() & 0xF, entry.getPosition().getY() & 0xF, entry.getPosition().getZ() & 0xF, entry.getBlock());
}
Comment thread
onebeastchris marked this conversation as resolved.

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;
Comment on lines +96 to +99
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specialization check !(blockState.block().getClass().equals(Block.class)) treats all Block subclasses as requiring per-block updateBlock(...) handling, even when the subclass does not override updateBlock (e.g., BannerBlock just adds dyeColor() but inherits Block.updateBlock). That means those blocks will still fan out into individual UpdateBlockPackets and reduce the effectiveness of batching. Consider switching to an explicit marker (interface/boolean method) for blocks that truly need special update behavior, and batch everything else.

Copilot uses AI. Check for mistakes.
}
Comment thread
valaphee marked this conversation as resolved.

// Skull is gone
session.getSkullCache().removeSkull(entry.getPosition());
Comment on lines +87 to +103
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you didn't use ChunkUtils#updateBlockClientSide as this would send UpdateBlockPacket for each changed block. But if we ever add something extra to Block#updateBlock (other than checkForEmptySkull) then we'd need to think about adding it here too.
This would probably require a larger refactor, so I think it's fine for now.
Not sure what other people think.

Copy link
Copy Markdown
Member Author

@valaphee valaphee Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea its only called if its specialized, for anything else we would have to check both locations, could add comments

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine with a comment in Block#updateBlock like: any changes you do here, add them to this translator too.


updateSubChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
entry.getPosition(),
session.getBlockMappings().getBedrockBlock(blockState),
FLAG_ALL,
-1,
MessageType.NONE
));
Comment thread
valaphee marked this conversation as resolved.
Comment thread
valaphee marked this conversation as resolved.

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
));
}
}

Comment thread
valaphee marked this conversation as resolved.
if (!updateSubChunkBlocksPacket.getStandardBlocks().isEmpty()) {
session.sendUpstreamPacket(updateSubChunkBlocksPacket);
}
}
}
Loading