diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java index 9258cd3b8dc..89f754e6b3d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/BasePiglinEntity.java @@ -32,7 +32,6 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import java.util.UUID; @@ -41,8 +40,8 @@ public class BasePiglinEntity extends MonsterEntity { public BasePiglinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); - // 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()); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java index b35e6a17f99..bbdaefed253 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java @@ -74,7 +74,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); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index e4296d7fadb..c9df45a4fdc 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -65,6 +65,7 @@ import org.geysermc.geyser.registry.populator.conversion.Conversion844_827; import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.GeyserBedrockBlock; +import org.geysermc.geyser.util.BlockHashUtils; import org.geysermc.geyser.util.JsonUtils; import java.io.DataInputStream; @@ -137,30 +138,31 @@ private static void registerBedrockBlocks() { //noinspection UnstableApiUsage Interner statesInterner = Interners.newStrongInterner(); - for (ObjectIntPair palette : blockMappers.keySet()) { - int protocolVersion = palette.valueInt(); - List vanillaBlockStates; - List blockStates; - try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow(String.format("bedrock/block_palette.%s.nbt", palette.key())); - NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) { - NbtMap blockPalette = (NbtMap) nbtInputStream.readTag(); + List vanillaBlockStates; + List blockStates; + 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(); + + vanillaBlockStates = new ArrayList<>(blockPalette.getList("blocks", NbtType.COMPOUND)); + for (int i = 0; i < vanillaBlockStates.size(); i++) { + NbtMapBuilder builder = vanillaBlockStates.get(i).toBuilder(); + builder.remove("version"); // Remove all nbt tags which are not needed for differentiating states + builder.remove("name_hash"); // Quick workaround - was added in 1.19.20 + builder.remove("network_id"); // Added in 1.19.80 + builder.remove("block_id"); // Added in 1.20.60 + //noinspection UnstableApiUsage + builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states"))); + vanillaBlockStates.set(i, builder.build()); + } - vanillaBlockStates = new ArrayList<>(blockPalette.getList("blocks", NbtType.COMPOUND)); - for (int i = 0; i < vanillaBlockStates.size(); i++) { - NbtMapBuilder builder = vanillaBlockStates.get(i).toBuilder(); - builder.remove("version"); // Remove all nbt tags which are not needed for differentiating states - builder.remove("name_hash"); // Quick workaround - was added in 1.19.20 - builder.remove("network_id"); // Added in 1.19.80 - builder.remove("block_id"); // Added in 1.20.60 - //noinspection UnstableApiUsage - builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states"))); - vanillaBlockStates.set(i, builder.build()); - } + blockStates = new ArrayList<>(vanillaBlockStates); + } catch (Exception e) { + throw new AssertionError("Unable to get blocks from runtime block states", e); + } - blockStates = new ArrayList<>(vanillaBlockStates); - } catch (Exception e) { - throw new AssertionError("Unable to get blocks from runtime block states", e); - } + for (ObjectIntPair palette : blockMappers.keySet()) { + int protocolVersion = palette.valueInt(); List customBlockProperties = new ArrayList<>(); List customBlockStates = new ArrayList<>(); @@ -179,17 +181,19 @@ private static void registerBedrockBlocks() { blockStates.sort((a, b) -> Long.compareUnsigned(fnv164(a.getString("name")), fnv164(b.getString("name")))); } - // 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 + final Remapper stateMapper = blockMappers.get(palette); + Object2ObjectMap 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) { + Int2ObjectMap bedrockHashedIdMap = new Int2ObjectOpenHashMap<>(); + for (final NbtMap tag : blockStates) { + NbtMap remappedTag = stateMapper.remap(tag); + + int hashedId = BlockHashUtils.toHash(remappedTag); + GeyserBedrockBlock block = new GeyserBedrockBlock(hashedId, remappedTag); + if (blockStateOrderedMap.put(tag, block) != null) { // Not a typo, use the latest block tag. throw new AssertionError("Duplicate block states in Bedrock palette: " + tag); } - bedrockRuntimeMap[i] = block; + bedrockHashedIdMap.put(hashedId, block); } Object2ObjectMap customBlockStateDefinitions = Object2ObjectMaps.emptyMap(); @@ -229,8 +233,6 @@ private static void registerBedrockBlocks() { BlockDefinition movingBlockDefinition = null; Iterator blocksIterator = BLOCKS_NBT.iterator(); - Remapper stateMapper = blockMappers.get(palette); - GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[JAVA_BLOCKS_SIZE]; GeyserBedrockBlock[] javaToVanillaBedrockBlocks = new GeyserBedrockBlock[JAVA_BLOCKS_SIZE]; @@ -261,8 +263,7 @@ 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); @@ -271,11 +272,7 @@ private static void registerBedrockBlocks() { 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)); + throw new RuntimeException("Unable to find %s Bedrock runtime ID for %s! Block tag: %s".formatted(javaId, palette.key(), bedrockTag)); } } else { bedrockDefinition = customBlockStateDefinitions.get(blockStateOverride); @@ -330,7 +327,7 @@ private static void registerBedrockBlocks() { // Get the tag needed for non-empty flower pots if (javaPottable.contains(block)) { // Specifically NOT putIfAbsent - mangrove propagule breaks otherwise - flowerPotBlocks.put(block, blockStates.get(bedrockDefinition.getRuntimeId())); + flowerPotBlocks.put(block, bedrockHashedIdMap.get(bedrockDefinition.getRuntimeId()).getState()); } javaToVanillaBedrockBlocks[javaRuntimeId] = vanillaBedrockDefinition; @@ -407,7 +404,7 @@ private static void registerBedrockBlocks() { } }); - BlockRegistries.BLOCKS.register(palette.valueInt(), builder.bedrockRuntimeMap(bedrockRuntimeMap) + BlockRegistries.BLOCKS.register(palette.valueInt(), builder.bedrockHashedIdMap(bedrockHashedIdMap) .javaToBedrockBlocks(javaToBedrockBlocks) .javaToVanillaBedrockBlocks(javaToVanillaBedrockBlocks) .javaToBedrockIdentifiers(javaToBedrockIdentifiers) diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index e68783e4954..4458fbd057b 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -361,7 +361,7 @@ public static void populate() { BlockDefinition bedrockBlock = null; Integer firstBlockRuntimeId = entry.getValue().getFirstBlockRuntimeId(); - BlockDefinition customBlockItemOverride = null; + GeyserBedrockBlock customBlockItemOverride = null; if (firstBlockRuntimeId != null) { BlockDefinition blockOverride = bedrockBlockIdOverrides.get(bedrockIdentifier); @@ -439,7 +439,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.getBedrockHashedIdMap().values()) { if (block == null) { continue; } diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java index 22679ecfdc5..3088bee35c2 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java @@ -60,7 +60,7 @@ public class BlockMappings implements DefinitionRegistry { Int2ObjectMap javaToBedrockIdentifiers; Map stateDefinitionMap; - GeyserBedrockBlock[] bedrockRuntimeMap; + Int2ObjectMap bedrockHashedIdMap; int[] remappedVanillaIds; BlockDefinition commandBlock; @@ -123,10 +123,7 @@ 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 this.bedrockHashedIdMap.get(bedrockId); } public @Nullable GeyserBedrockBlock getDefinition(NbtMap tag) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserBedrockBlock.java b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserBedrockBlock.java index 190354359df..0c471a57a1a 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserBedrockBlock.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserBedrockBlock.java @@ -25,25 +25,23 @@ package org.geysermc.geyser.registry.type; +import lombok.Getter; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; public class GeyserBedrockBlock implements BlockDefinition { - private final int runtimeId; + private final int hashedNetworkId; + @Getter private final NbtMap state; - public GeyserBedrockBlock(int runtimeId, NbtMap state) { - this.runtimeId = runtimeId; + public GeyserBedrockBlock(int hashedNetworkId, NbtMap state) { + this.hashedNetworkId = hashedNetworkId; this.state = state; } @Override - public int getRuntimeId() { - return runtimeId; - } - - public NbtMap getState() { - return state; + public int getRuntimeId() { // A little bit misleading name lol. + return this.hashedNetworkId; } @Override diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index ecdf47fb147..787096b6d14 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1770,7 +1770,9 @@ private void startGame() { startGamePacket.setCustomBiomeName(""); startGamePacket.setEducationProductionId(""); startGamePacket.setForceExperimentalGameplay(OptionalBoolean.empty()); - + + startGamePacket.setBlockNetworkIdsHashed(true); + String serverName = geyser.config().gameplay().serverName(); startGamePacket.setLevelId(serverName); startGamePacket.setLevelName(serverName); diff --git a/core/src/main/java/org/geysermc/geyser/util/BlockHashUtils.java b/core/src/main/java/org/geysermc/geyser/util/BlockHashUtils.java new file mode 100644 index 00000000000..e0c2b003a5f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/util/BlockHashUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 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.util; + +import org.cloudburstmc.nbt.NBTOutputStream; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.TreeMap; + +// Taken from https://gist.github.com/Alemiz112/504d0f79feac7ef57eda174b668dd345 +public class BlockHashUtils { + private static final int FNV1_32_INIT = 0x811c9dc5; + private static final int FNV1_PRIME_32 = 0x01000193; + + public static int toHash(NbtMap block) { + if (block.getString("name").equals("minecraft:unknown")) { + return -2; // This is special case + } + + NbtMap tag = NbtMap.builder() + .putString("name", block.getString("name")) + .putCompound("states", NbtMap.fromMap( + new TreeMap<>(block.getCompound("states")))) + .build(); + + byte[] bytes; + try (ByteArrayOutputStream stream = new ByteArrayOutputStream(); + NBTOutputStream outputStream = NbtUtils.createWriterLE(stream)) { + outputStream.writeTag(tag); + bytes = stream.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return fnv1a_32(bytes); + } + + private static int fnv1a_32(byte[] data) { + int hash = FNV1_32_INIT; + for (byte datum : data) { + hash ^= (datum & 0xff); + hash *= FNV1_PRIME_32; + } + return hash; + } +} diff --git a/core/src/main/resources/bedrock/block_palette.1_21_100.nbt b/core/src/main/resources/bedrock/block_palette.1_21_100.nbt deleted file mode 100644 index a360e09ca0c..00000000000 Binary files a/core/src/main/resources/bedrock/block_palette.1_21_100.nbt and /dev/null differ diff --git a/core/src/main/resources/bedrock/block_palette.1_21_90.nbt b/core/src/main/resources/bedrock/block_palette.1_21_90.nbt deleted file mode 100644 index def2068765e..00000000000 Binary files a/core/src/main/resources/bedrock/block_palette.1_21_90.nbt and /dev/null differ diff --git a/core/src/main/resources/bedrock/block_palette.1_21_110.nbt b/core/src/main/resources/bedrock/block_palette.nbt similarity index 100% rename from core/src/main/resources/bedrock/block_palette.1_21_110.nbt rename to core/src/main/resources/bedrock/block_palette.nbt