Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use UpdateSubChunkBlocksPacket #4659

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ public void updateServerCorrectBlockState(Vector3i position, int blockState) {
ChunkUtils.updateBlock(session, blockState, position);
}

public void removePrediction(Vector3i position) {
if (!this.unverifiedPredictions.isEmpty()) {
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,191 @@

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

import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateSubChunkBlocksPacket;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
import org.geysermc.geyser.util.BlockEntityUtils;
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 static org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry.MessageType;
import static org.geysermc.geyser.level.block.BlockStateValues.JAVA_AIR_ID;

@Translator(packet = ClientboundSectionBlocksUpdatePacket.class)
public class JavaSectionBlocksUpdateTranslator extends PacketTranslator<ClientboundSectionBlocksUpdatePacket> {
private static final int NEIGHBORS_NETWORK_FLAG = (1 << UpdateBlockPacket.Flag.NEIGHBORS.ordinal()) | (1 << UpdateBlockPacket.Flag.NETWORK.ordinal());
private static final int NETWORK_FLAG = (1 << UpdateBlockPacket.Flag.NETWORK.ordinal());

@Override
public void translate(GeyserSession session, ClientboundSectionBlocksUpdatePacket packet) {
// Send normal block updates if not many changes
if (packet.getEntries().length < 32) {
for (BlockChangeEntry entry : packet.getEntries()) {
session.getWorldCache().updateServerCorrectBlockState(entry.getPosition(), entry.getBlock());
}
return;
}

UpdateSubChunkBlocksPacket subChunkBlocksPacket = new UpdateSubChunkBlocksPacket();
subChunkBlocksPacket.setChunkX(packet.getChunkX());
subChunkBlocksPacket.setChunkY(packet.getChunkY());
subChunkBlocksPacket.setChunkZ(packet.getChunkZ());

// If the entire section is updated, this might be a legacy non-full chunk update
// which can contain thousands of unchanged blocks
if (packet.getEntries().length == 4096 && !session.getGeyser().getWorldManager().hasOwnChunkCache()) {
// hack - bedrock might ignore the block updates if the chunk was still loading.
// sending an UpdateBlockPacket seems to force it
BlockChangeEntry firstEntry = packet.getEntries()[0];
UpdateBlockPacket blockPacket = new UpdateBlockPacket();
blockPacket.setBlockPosition(firstEntry.getPosition());
blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(firstEntry.getBlock()));
blockPacket.setDataLayer(0);
session.sendUpstreamPacket(blockPacket);

// Filter out unchanged blocks
Vector3i offset = Vector3i.from(packet.getChunkX() << 4, packet.getChunkY() << 4, packet.getChunkZ() << 4);
BlockPositionIterator blockIter = BlockPositionIterator.fromMinMax(
offset.getX(), offset.getY(), offset.getZ(),
offset.getX() + 15, offset.getY() + 15, offset.getZ() + 15
);

int[] sectionBlocks = session.getGeyser().getWorldManager().getBlocksAt(session, blockIter);
Copy link
Member

Choose a reason for hiding this comment

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

This will probably break between integrated and standalone world managers. Standalone will have the cache, Spigot will have the live blocks.

BitSet waterlogged = BlockRegistries.WATERLOGGED.get();
for (BlockChangeEntry entry : packet.getEntries()) {
Vector3i pos = entry.getPosition().sub(offset);
int index = pos.getZ() + pos.getX() * 16 + pos.getY() * 256;
int oldBlockState = sectionBlocks[index];
if (oldBlockState != entry.getBlock()) {
// Avoid sending unnecessary waterlogged updates
boolean updateWaterlogged = waterlogged.get(oldBlockState) != waterlogged.get(entry.getBlock());
applyEntry(session, entry, subChunkBlocksPacket, updateWaterlogged);
}
}
} else {
for (BlockChangeEntry entry : packet.getEntries()) {
applyEntry(session, entry, subChunkBlocksPacket, true);
}
}

session.sendUpstreamPacket(subChunkBlocksPacket);

// Post block update
for (BlockChangeEntry entry : packet.getEntries()) {
session.getWorldCache().updateServerCorrectBlockState(entry.getPosition(), entry.getBlock());
session.getWorldCache().removePrediction(entry.getPosition());
BlockStateValues.getLecternBookStates().handleBlockChange(session, entry.getBlock(), entry.getPosition());

// Iterates through all Bedrock-only block entity translators and determines if a manual block entity packet
// needs to be sent
for (BedrockOnlyBlockEntity bedrockOnlyBlockEntity : BlockEntityUtils.BEDROCK_ONLY_BLOCK_ENTITIES) {
if (bedrockOnlyBlockEntity.isBlock(entry.getBlock())) {
// Flower pots are block entities only in Bedrock and are not updated anywhere else like note blocks
bedrockOnlyBlockEntity.updateBlock(session, entry.getBlock(), entry.getPosition());
break; //No block will be a part of two classes
}
}
}
}

// Modified version of ChunkUtils#updateBlockClientSide
private static void applyEntry(GeyserSession session, BlockChangeEntry entry, UpdateSubChunkBlocksPacket subChunkBlocksPacket, boolean updateWaterlogged) {
Vector3i position = entry.getPosition();
int blockState = entry.getBlock();

session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);

// 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 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
}

BlockDefinition definition = session.getBlockMappings().getBedrockBlock(blockState);

int skullVariant = BlockStateValues.getSkullVariant(blockState);
if (skullVariant == -1) {
// Skull is gone
session.getSkullCache().removeSkull(position);
} else if (skullVariant == 3) {
// The changed block was a player skull so check if a custom block was defined for this skull
SkullCache.Skull skull = session.getSkullCache().updateSkull(position, blockState);
if (skull != null && skull.getBlockDefinition() != null) {
definition = skull.getBlockDefinition();
}
}

// Prevent moving_piston from being placed
// It's used for extending piston heads, but it isn't needed on Bedrock and causes pistons to flicker
if (!BlockStateValues.isMovingPiston(blockState)) {
subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
position,
definition,
NEIGHBORS_NETWORK_FLAG,
-1,
MessageType.NONE
));

if (updateWaterlogged) {
BlockDefinition waterDefinition = BlockRegistries.WATERLOGGED.get().get(blockState) ?
session.getBlockMappings().getBedrockWater() : session.getBlockMappings().getBedrockAir();
subChunkBlocksPacket.getExtraBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
position,
waterDefinition,
0,
-1,
MessageType.NONE
));
}
}

// Extended collision boxes for custom blocks
if (!session.getBlockMappings().getExtendedCollisionBoxes().isEmpty()) {
int aboveBlock = session.getGeyser().getWorldManager().getBlockAt(session, position.getX(), position.getY() + 1, position.getZ());
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(blockState);
int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, position.getX(), position.getY() - 1, position.getZ());
BlockDefinition belowBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock);
if (belowBedrockExtendedCollisionDefinition != null && blockState == BlockStateValues.JAVA_AIR_ID) {
subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
position,
belowBedrockExtendedCollisionDefinition,
NETWORK_FLAG,
-1,
MessageType.NONE
));
} else if (aboveBedrockExtendedCollisionDefinition != null && aboveBlock == BlockStateValues.JAVA_AIR_ID) {
subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
position.add(0, 1, 0),
aboveBedrockExtendedCollisionDefinition,
NETWORK_FLAG,
-1,
MessageType.NONE
));
} else if (aboveBlock == BlockStateValues.JAVA_AIR_ID) {
subChunkBlocksPacket.getStandardBlocks().add(new org.cloudburstmc.protocol.bedrock.data.BlockChangeEntry(
position.add(0, 1, 0),
session.getBlockMappings().getBedrockAir(),
NETWORK_FLAG,
-1,
MessageType.NONE
));
}
}
}
}