Skip to content
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 @@ -34,6 +34,7 @@
import org.cloudburstmc.protocol.bedrock.packet.BlockEntityDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
import org.geysermc.geyser.entity.spawn.EntitySpawnContext;
import org.geysermc.geyser.registry.type.GeyserBedrockBlock;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.geyser.util.InteractionResult;
Expand Down Expand Up @@ -226,7 +227,7 @@ private BlockDefinition buildBlockDefinition(Direction direction) {
.putByte("item_frame_photo_bit", (byte) 0);
blockBuilder.put("states", statesBuilder.build());

return session.getBlockMappings().getItemFrame(blockBuilder.build());
return new GeyserBedrockBlock(blockBuilder.build());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public class BasePiglinEntity extends MonsterEntity {

public BasePiglinEntity(EntitySpawnContext context) {
super(context);
// Both TARGET_EID and BLOCK are needed for melee attack animation
dirtyMetadata.put(EntityDataTypes.BLOCK, session.getBlockMappings().getDefinition(1));
// Both TARGET_EID and VARIANT are needed for melee attack animation
dirtyMetadata.put(EntityDataTypes.VARIANT, 1);
setFlag(EntityFlag.SHAKING, isShaking());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void setHand(GeyserItemStack stack) {
boolean toCrossbow = stack != null && stack.is(Items.CROSSBOW);

if (toCrossbow ^ getMainHandItem().is(Items.CROSSBOW)) { // If switching to/from crossbow
dirtyMetadata.put(EntityDataTypes.BLOCK, session.getBlockMappings().getDefinition(toCrossbow ? 0 : 1));
dirtyMetadata.put(EntityDataTypes.VARIANT, toCrossbow ? 0 : 1);
dirtyMetadata.put(EntityDataTypes.CHARGE_AMOUNT, (byte) 0);
setFlag(EntityFlag.CHARGED, false);
setFlag(EntityFlag.USING_ITEM, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public PacketSignal handle(LoginPacket loginPacket) {
}

// Set the block translation based off of version
session.setBlockMappings(BlockRegistries.BLOCKS.forVersion(loginPacket.getProtocolVersion()));
session.setBlockMappings(BlockRegistries.BLOCKS.get());
session.setItemMappings(Registries.ITEMS.forVersion(loginPacket.getProtocolVersion()));

LoginEncryptionUtils.encryptPlayerConnection(session, loginPacket);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@
*/
public class BlockRegistries {
/**
* A versioned registry which holds {@link BlockMappings} for each version. These block mappings contain
* primarily Bedrock version-specific data.
* A registry which holds {@link BlockMappings}.
*/
public static final VersionedRegistry<BlockMappings> BLOCKS = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
public static final SimpleRegistry<BlockMappings> BLOCKS = SimpleRegistry.create(RegistryLoaders.uninitialized());

/**
* A registry which stores Java IDs to Java {@link BlockState}s, each with their specific state differences and a link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,24 @@

package org.geysermc.geyser.registry.populator;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIntPair;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.cloudburstmc.nbt.NBTInputStream;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType;
import org.cloudburstmc.nbt.NbtUtils;
import org.cloudburstmc.protocol.bedrock.codec.v898.Bedrock_v898;
import org.cloudburstmc.protocol.bedrock.codec.v924.Bedrock_v924;
import org.cloudburstmc.protocol.bedrock.codec.v944.Bedrock_v944;
import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.geysermc.geyser.GeyserImpl;
Expand All @@ -64,7 +61,6 @@

import java.io.DataInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
Expand Down Expand Up @@ -117,24 +113,14 @@ private static void nullifyBlocksNbt() {
}

private static void registerBedrockBlocks() {
var blockMappers = ImmutableMap.<ObjectIntPair<String>, Remapper>builder()
// This is technically the same 1.21.111 palette; there have been no changes
.put(ObjectIntPair.of("1_21_130", Bedrock_v898.CODEC.getProtocolVersion()), tag -> tag)
// 26.0 also doesn't have any changes, so we re-use the same file
.put(ObjectIntPair.of("1_21_130", Bedrock_v924.CODEC.getProtocolVersion()), tag -> tag)
.put(ObjectIntPair.of("1_26_10", Bedrock_v944.CODEC.getProtocolVersion()), tag -> tag)
.build();

// We can keep this strong as nothing should be garbage collected
// Safe to intern since Cloudburst NBT is immutable
//noinspection UnstableApiUsage
Interner<NbtMap> statesInterner = Interners.newStrongInterner();

for (ObjectIntPair<String> palette : blockMappers.keySet()) {
int protocolVersion = palette.valueInt();
List<NbtMap> vanillaBlockStates;
List<NbtMap> blockStates;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(String.format("bedrock/block_palette.%s.nbt", palette.key()));
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("bedrock/block_palette.nbt");
NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) {
NbtMap blockPalette = (NbtMap) nbtInputStream.readTag();

Expand Down Expand Up @@ -166,23 +152,15 @@ private static void registerBedrockBlocks() {
CustomBlockRegistryPopulator.generateCustomBlockStates(customBlock, customBlockStates, customExtBlockStates);
}
blockStates.addAll(customBlockStates);
GeyserImpl.getInstance().getLogger().debug("Added " + customBlockStates.size() + " custom block states to v" + protocolVersion + " palette.");

// The palette is sorted by the FNV1 64-bit hash of the name
blockStates.sort((a, b) -> Long.compareUnsigned(fnv164(a.getString("name")), fnv164(b.getString("name"))));
GeyserImpl.getInstance().getLogger().debug("Added " + customBlockStates.size() + " custom block states.");
}

// New since 1.16.100 - find the block runtime ID by the order given to us in the block palette,
// as we no longer send a block palette
Object2ObjectMap<NbtMap, GeyserBedrockBlock> blockStateOrderedMap = new Object2ObjectOpenHashMap<>(blockStates.size());
GeyserBedrockBlock[] bedrockRuntimeMap = new GeyserBedrockBlock[blockStates.size()];
for (int i = 0; i < blockStates.size(); i++) {
NbtMap tag = blockStates.get(i);
GeyserBedrockBlock block = new GeyserBedrockBlock(i, tag);
if (blockStateOrderedMap.put(tag, block) != null) {
throw new AssertionError("Duplicate block states in Bedrock palette: " + tag);
}
bedrockRuntimeMap[i] = block;
Int2ObjectMap<GeyserBedrockBlock> bedrockRuntimeMap = new Int2ObjectOpenHashMap<>(blockStates.size());
for (NbtMap tag : blockStates) {
GeyserBedrockBlock block = new GeyserBedrockBlock(tag);
bedrockRuntimeMap.put(block.getRuntimeId(), block);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

bedrockRuntimeMap.put(block.getRuntimeId(), block) will silently overwrite if two distinct block states hash to the same runtime ID. Since these IDs are now computed hashes (not sequential), it would be safer to detect collisions (e.g., check the previous value returned by put, and if present and the state differs, throw/log) to avoid hard-to-debug client desyncs.

Suggested change
bedrockRuntimeMap.put(block.getRuntimeId(), block);
GeyserBedrockBlock previous = bedrockRuntimeMap.put(block.getRuntimeId(), block);
if (previous != null) {
GeyserImpl.getInstance().getLogger().error(
"Detected Bedrock runtime ID collision for ID {}. Previous block: {}, New block: {}",
block.getRuntimeId(), previous, block
);
throw new IllegalStateException("Bedrock runtime ID collision detected for ID " + block.getRuntimeId());
}

Copilot uses AI. Check for mistakes.
}

Object2ObjectMap<CustomBlockState, GeyserBedrockBlock> customBlockStateDefinitions = Object2ObjectMaps.emptyMap();
Expand All @@ -191,14 +169,14 @@ private static void registerBedrockBlocks() {
for (int i = 0; i < customExtBlockStates.size(); i++) {
NbtMap tag = customBlockStates.get(i);
CustomBlockState blockState = customExtBlockStates.get(i);
GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(tag);
GeyserBedrockBlock bedrockBlock = new GeyserBedrockBlock(tag);
customBlockStateDefinitions.put(blockState, bedrockBlock);
}

remappedVanillaIds = new int[vanillaBlockStates.size()];
for (int i = 0; i < vanillaBlockStates.size(); i++) {
GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(vanillaBlockStates.get(i));
remappedVanillaIds[i] = bedrockBlock != null ? bedrockBlock.getRuntimeId() : -1;
GeyserBedrockBlock bedrockBlock = new GeyserBedrockBlock(vanillaBlockStates.get(i));
remappedVanillaIds[i] = bedrockBlock.getRuntimeId();
}
}

Expand All @@ -214,8 +192,6 @@ private static void registerBedrockBlocks() {
BlockDefinition movingBlockDefinition = null;
Iterator<NbtMap> blocksIterator = BLOCKS_NBT.iterator();

Remapper stateMapper = blockMappers.get(palette);

GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[JAVA_BLOCKS_SIZE];
GeyserBedrockBlock[] javaToVanillaBedrockBlocks = new GeyserBedrockBlock[JAVA_BLOCKS_SIZE];

Expand All @@ -233,7 +209,7 @@ private static void registerBedrockBlocks() {
})
.toList();
Map<Block, NbtMap> flowerPotBlocks = new Object2ObjectOpenHashMap<>();
Map<NbtMap, BlockDefinition> itemFrames = new Object2ObjectOpenHashMap<>();
IntOpenHashSet itemFrames = new IntOpenHashSet();
IntArrayList collisionIgnoredBlocks = new IntArrayList();

Set<BlockDefinition> jigsawDefinitions = new ObjectOpenHashSet<>();
Expand All @@ -246,28 +222,16 @@ private static void registerBedrockBlocks() {
BlockState blockState = javaBlockStates.get(javaRuntimeId);
String javaId = blockState.toString();

NbtMap originalBedrockTag = buildBedrockState(blockState, entry);
NbtMap bedrockTag = stateMapper.remap(originalBedrockTag);
NbtMap bedrockTag = buildBedrockState(blockState, entry);

GeyserBedrockBlock vanillaBedrockDefinition = blockStateOrderedMap.get(bedrockTag);
GeyserBedrockBlock vanillaBedrockDefinition = new GeyserBedrockBlock(bedrockTag);

GeyserBedrockBlock bedrockDefinition;
Comment on lines +225 to 229
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

With hashed block network IDs, bedrockDefinition.getRuntimeId() is no longer a dense 0..N index. In this method there's still logic later on that uses getRuntimeId() to index into blockStates when populating flowerPotBlocks, which will now be out-of-range/negative and can break registry population or store the wrong tag. That lookup should be replaced with a map-based lookup (e.g., using the computed tag itself or bedrockRuntimeMap.get(runtimeId)), not a list index.

Copilot uses AI. Check for mistakes.
CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(javaRuntimeId);
if (blockStateOverride == null) {
bedrockDefinition = vanillaBedrockDefinition;
if (bedrockDefinition == null) {
throw new RuntimeException("""
Unable to find %s Bedrock runtime ID for %s! Original block tag:
%s
Updated block tag:
%s""".formatted(javaId, palette.key(), originalBedrockTag, bedrockTag));
}
} else {
bedrockDefinition = customBlockStateDefinitions.get(blockStateOverride);
if (bedrockDefinition == null) {
throw new RuntimeException("Unable to find " + javaId + " Bedrock runtime ID! Custom block override: \n" +
blockStateOverride);
}
}

switch (javaId) {
Expand Down Expand Up @@ -385,18 +349,17 @@ private static void registerBedrockBlocks() {
javaToBedrockIdentifiers.trim();

// Loop around again to find all item frame runtime IDs
Object2ObjectMaps.fastForEach(blockStateOrderedMap, entry -> {
String name = entry.getKey().getString("name");
for (NbtMap entry : blockStates) {
String name = entry.getString("name");
if (name.equals("minecraft:frame") || name.equals("minecraft:glow_frame")) {
itemFrames.put(entry.getKey(), entry.getValue());
itemFrames.add(new GeyserBedrockBlock(entry).getRuntimeId());
}
});
}

BlockRegistries.BLOCKS.register(palette.valueInt(), builder.bedrockRuntimeMap(bedrockRuntimeMap)
BlockRegistries.BLOCKS.set(builder.bedrockRuntimeMap(bedrockRuntimeMap)
.javaToBedrockBlocks(javaToBedrockBlocks)
.javaToVanillaBedrockBlocks(javaToVanillaBedrockBlocks)
.javaToBedrockIdentifiers(javaToBedrockIdentifiers)
.stateDefinitionMap(blockStateOrderedMap)
.itemFrames(itemFrames)
.flowerPotBlocks(flowerPotBlocks)
.jigsawStates(jigsawDefinitions)
Expand All @@ -405,7 +368,6 @@ private static void registerBedrockBlocks() {
.blockProperties(customBlockProperties)
.customBlockStateDefinitions(customBlockStateDefinitions)
.build());
}
}

private static void registerJavaBlocks() {
Expand Down Expand Up @@ -454,22 +416,4 @@ private static NbtMap buildBedrockState(BlockState state, NbtMap nbt) {
tagBuilder.put("states", nbt.getCompound("state"));
return tagBuilder.build();
}

private static final long FNV1_64_OFFSET_BASIS = 0xcbf29ce484222325L;
private static final long FNV1_64_PRIME = 1099511628211L;

/**
* Hashes a string using the FNV-1a 64-bit algorithm.
*
* @param str The string to hash
* @return The hashed string
*/
private static long fnv164(String str) {
long hash = FNV1_64_OFFSET_BASIS;
for (byte b : str.getBytes(StandardCharsets.UTF_8)) {
hash *= FNV1_64_PRIME;
hash ^= b;
}
return hash;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ static void populate(ItemRegistryPopulator.PaletteVersion palette, Map<String, I
throw new AssertionError("Unable to load creative items", e);
}

BlockMappings blockMappings = BlockRegistries.BLOCKS.forVersion(palette.protocolVersion());
BlockMappings blockMappings = BlockRegistries.BLOCKS.get();
for (JsonElement itemNode : creativeItemEntries) {
ItemData.Builder itemBuilder = createItemData((JsonObject) itemNode, items, blockMappings, definitions);
if (itemBuilder == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public static void populate() {
Map<CreativeItemCategory, Integer> lastCreativeGroupIds = new Object2IntOpenHashMap<>();
CreativeItemRegistryPopulator.readCreativeItemGroups(palette, creativeItems, creativeItemGroups, creativeGroupIds, lastCreativeGroupIds);

BlockMappings blockMappings = BlockRegistries.BLOCKS.forVersion(palette.protocolVersion());
BlockMappings blockMappings = BlockRegistries.BLOCKS.get();

Set<Item> javaOnlyItems = new ObjectOpenHashSet<>();
Collections.addAll(javaOnlyItems, Items.SPECTRAL_ARROW, Items.DEBUG_STICK,
Expand Down Expand Up @@ -391,7 +391,7 @@ public static void populate() {
if (bedrockBlock == null) {
// We need to loop around again (we can't cache the block tags above) because Bedrock can include states that we don't have a pairing for
// in it's "preferred" block state - I.E. the first matching block state in the list
for (GeyserBedrockBlock block : blockMappings.getBedrockRuntimeMap()) {
for (GeyserBedrockBlock block : blockMappings.getBedrockRuntimeMap().values()) {
if (block == null) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import lombok.Builder;
import lombok.Value;
Expand Down Expand Up @@ -59,8 +60,7 @@ public class BlockMappings implements DefinitionRegistry<BlockDefinition> {
*/
Int2ObjectMap<String> javaToBedrockIdentifiers;

Map<NbtMap, GeyserBedrockBlock> stateDefinitionMap;
GeyserBedrockBlock[] bedrockRuntimeMap;
Int2ObjectMap<GeyserBedrockBlock> bedrockRuntimeMap;
int[] remappedVanillaIds;

BlockDefinition commandBlock;
Expand All @@ -69,7 +69,7 @@ public class BlockMappings implements DefinitionRegistry<BlockDefinition> {

IntArrayList collisionIgnoredBlocks;

Map<NbtMap, BlockDefinition> itemFrames;
IntOpenHashSet itemFrames;
Map<Block, NbtMap> flowerPotBlocks;

Set<BlockDefinition> jigsawStates;
Expand Down Expand Up @@ -105,16 +105,8 @@ public GeyserBedrockBlock getVanillaBedrockBlock(int javaState) {
return this.javaToVanillaBedrockBlocks[javaState];
}

public BlockDefinition getItemFrame(NbtMap tag) {
return this.itemFrames.get(tag);
}

public boolean isItemFrame(BlockDefinition definition) {
if (definition instanceof GeyserBedrockBlock def) {
return this.itemFrames.containsKey(def.getState());
}

return false;
return this.itemFrames.contains(definition.getRuntimeId());
}

public BlockDefinition getStructureBlockFromMode(String mode) {
Expand All @@ -123,18 +115,15 @@ public BlockDefinition getStructureBlockFromMode(String mode) {

@Override
public @Nullable GeyserBedrockBlock getDefinition(int bedrockId) {
if (bedrockId < 0 || bedrockId >= this.bedrockRuntimeMap.length) {
return null;
}
return bedrockRuntimeMap[bedrockId];
return bedrockRuntimeMap.get(bedrockId);
}

public @Nullable GeyserBedrockBlock getDefinition(NbtMap tag) {
if (tag == null) {
return null;
}

return this.stateDefinitionMap.get(tag);
return new GeyserBedrockBlock(tag);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

getDefinition(NbtMap tag) now always returns a new GeyserBedrockBlock instance and does not consult bedrockRuntimeMap. This makes the method behave differently from getDefinition(int) and also means isRegistered() will be false for values returned here. Consider computing the runtime ID from the tag and returning the canonical instance from bedrockRuntimeMap (or returning null if it isn't present) to keep registry semantics consistent and avoid repeated hashing/allocations.

Suggested change
return new GeyserBedrockBlock(tag);
// Construct a temporary block to determine the runtime ID, then
// return the canonical instance from the registry if present.
GeyserBedrockBlock tempBlock = new GeyserBedrockBlock(tag);
return bedrockRuntimeMap.get(tempBlock.getRuntimeId());

Copilot uses AI. Check for mistakes.
}

@Override
Expand Down
Loading
Loading