Skip to content
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,41 +69,40 @@ 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;
/**
* 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) {
GeyserChunk chunk = this.getChunk(chunkX, chunkZ);
if (chunk == null) {
return null;
}

GeyserChunk chunk = this.getChunk(x >> 4, z >> 4);
if (chunk == null) {
return;
if (chunkY < getChunkMinY() || chunkY - getChunkMinY() > chunk.sections().length - 1) {
return null;
}

if (y < minY || ((y - minY) >> 4) > chunk.sections().length - 1) {
// Y likely goes above or below the height limit of this world
DataPalette palette = chunk.sections()[chunkY - getChunkMinY()];
if (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 void updateBlock(int x, int y, int z, int block) {
if (!cache) {
return;
}

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 = this.getChunkSection(x >> 4, y >> 4, z >> 4);
if (palette == null) {
return;
Comment thread
valaphee marked this conversation as resolved.
Outdated
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,91 @@

package org.geysermc.geyser.translator.protocol.java.level;

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;

@Translator(packet = ClientboundSectionBlocksUpdatePacket.class)
public class JavaSectionBlocksUpdateTranslator extends PacketTranslator<ClientboundSectionBlocksUpdatePacket> {

@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());
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.

BitSet waterlogged = BlockRegistries.WATERLOGGED.get();

UpdateSubChunkBlocksPacket updateSubChunkBlocksPacket = new UpdateSubChunkBlocksPacket();

for (BlockChangeEntry entry : packet.getEntries()) {
session.getWorldCache().updateServerCorrectBlockState(entry.getPosition(), 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),
3,
Comment thread
valaphee marked this conversation as resolved.
Outdated
-1,
org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType.NONE
Comment thread
valaphee marked this conversation as resolved.
Outdated
));
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,
org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType.NONE
));
}
}

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