diff --git a/build.gradle.kts b/build.gradle.kts index e632e98c9..8ab695fe1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ plugins { `java-library` + // Ensure AP works in eclipse (no effect on other IDEs) + `eclipse` id("geyser.build-logic") id("io.freefair.lombok") version "6.3.0" apply false } diff --git a/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunkSection.java b/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunkSection.java index f3e6b8399..8a3534a8e 100644 --- a/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunkSection.java +++ b/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunkSection.java @@ -30,18 +30,20 @@ import org.cloudburstmc.protocol.common.util.Preconditions; public class GeyserChunkSection { - // Temporary reversion to v8 as it reduces the frequnecy of https://github.com/GeyserMC/Geyser/issues/4240 - // This does not fully resolve the issue so a better solution is still needed - private static final int CHUNK_SECTION_VERSION = 8; + // As of at least 1.19.80 + private static final int CHUNK_SECTION_VERSION = 9; private final BlockStorage[] storage; + // Counts up from 00 for y >= 0 and down from FF for y < 0 + private final int subChunkIndex; - public GeyserChunkSection(int airBlockId) { - this(new BlockStorage[]{new BlockStorage(airBlockId), new BlockStorage(airBlockId)}); + public GeyserChunkSection(int airBlockId, int subChunkIndex) { + this(new BlockStorage[]{new BlockStorage(airBlockId), new BlockStorage(airBlockId)}, subChunkIndex); } - public GeyserChunkSection(BlockStorage[] storage) { + public GeyserChunkSection(BlockStorage[] storage, int subChunkIndex) { this.storage = storage; + this.subChunkIndex = subChunkIndex; } public int getFullBlock(int x, int y, int z, int layer) { @@ -60,6 +62,7 @@ public class GeyserChunkSection { buffer.writeByte(CHUNK_SECTION_VERSION); buffer.writeByte(this.storage.length); // Required for chunk version 9+ + buffer.writeByte(this.subChunkIndex); for (BlockStorage blockStorage : this.storage) { blockStorage.writeToNetwork(buffer); } @@ -86,12 +89,12 @@ public class GeyserChunkSection { return true; } - public GeyserChunkSection copy() { + public GeyserChunkSection copy(int subChunkIndex) { BlockStorage[] storage = new BlockStorage[this.storage.length]; for (int i = 0; i < storage.length; i++) { storage[i] = this.storage[i].copy(); } - return new GeyserChunkSection(storage); + return new GeyserChunkSection(storage, subChunkIndex); } public static int blockPosition(int x, int y, int z) { 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 265c6b1f7..e3f4e685d 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 @@ -221,6 +221,7 @@ public final class BlockRegistryPopulator { GeyserBedrockBlock airDefinition = null; BlockDefinition commandBlockDefinition = null; + BlockDefinition mobSpawnerBlockDefinition = null; BlockDefinition waterDefinition = null; BlockDefinition movingBlockDefinition = null; Iterator> blocksIterator = BLOCKS_JSON.fields(); @@ -269,6 +270,7 @@ public final class BlockRegistryPopulator { case "minecraft:air" -> airDefinition = bedrockDefinition; case "minecraft:water[level=0]" -> waterDefinition = bedrockDefinition; case "minecraft:command_block[conditional=false,facing=north]" -> commandBlockDefinition = bedrockDefinition; + case "minecraft:spawner" -> mobSpawnerBlockDefinition = bedrockDefinition; case "minecraft:moving_piston[facing=north,type=normal]" -> movingBlockDefinition = bedrockDefinition; } @@ -298,9 +300,13 @@ public final class BlockRegistryPopulator { if (commandBlockDefinition == null) { throw new AssertionError("Unable to find command block in palette"); } - builder.commandBlock(commandBlockDefinition); + if (mobSpawnerBlockDefinition == null) { + throw new AssertionError("Unable to find mob spawner block in palette"); + } + builder.mobSpawnerBlock(mobSpawnerBlockDefinition); + if (waterDefinition == null) { throw new AssertionError("Unable to find water in palette"); } 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 80be3fbce..3d06cecac 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 @@ -54,6 +54,7 @@ public class BlockMappings implements DefinitionRegistry { int[] remappedVanillaIds; BlockDefinition commandBlock; + BlockDefinition mobSpawnerBlock; Map itemFrames; Map flowerPotBlocks; diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BlockEntityTranslator.java index b89a2a547..7566e0d90 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BlockEntityTranslator.java @@ -30,6 +30,7 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.BlockEntityUtils; /** @@ -41,7 +42,7 @@ public abstract class BlockEntityTranslator { public abstract void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState); - public NbtMap getBlockEntityTag(BlockEntityType type, int x, int y, int z, CompoundTag tag, int blockState) { + public NbtMap getBlockEntityTag(GeyserSession session, BlockEntityType type, int x, int y, int z, CompoundTag tag, int blockState) { NbtMapBuilder tagBuilder = getConstantBedrockTag(BlockEntityUtils.getBedrockBlockEntityId(type), x, y, z); translateTag(tagBuilder, tag, blockState); return tagBuilder.build(); diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java index d1af70d8d..0aa8af279 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SpawnerBlockEntityTranslator.java @@ -29,12 +29,44 @@ import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; +import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.GeyserSession; @BlockEntity(type = BlockEntityType.MOB_SPAWNER) public class SpawnerBlockEntityTranslator extends BlockEntityTranslator { + + @Override + public NbtMap getBlockEntityTag(GeyserSession session, BlockEntityType type, int x, int y, int z, CompoundTag tag, int blockState) { + // Sending an empty EntityIdentifier to empty the spawner is ignored by the client, so we send a whole new spawner! + // Fixes https://github.com/GeyserMC/Geyser/issues/4214 + CompoundTag spawnData = tag.get("SpawnData"); + if (spawnData != null) { + CompoundTag entityTag = spawnData.get("entity"); + if (entityTag.isEmpty()) { + Vector3i position = Vector3i.from(x, y, z); + // Set to air and back to reset the spawner - "just" updating the spawner doesn't work + UpdateBlockPacket emptyBlockPacket = new UpdateBlockPacket(); + emptyBlockPacket.setDataLayer(0); + emptyBlockPacket.setBlockPosition(position); + emptyBlockPacket.setDefinition(session.getBlockMappings().getBedrockAir()); + session.sendUpstreamPacket(emptyBlockPacket); + + UpdateBlockPacket spawnerBlockPacket = new UpdateBlockPacket(); + spawnerBlockPacket.setDataLayer(0); + spawnerBlockPacket.setBlockPosition(position); + spawnerBlockPacket.setDefinition(session.getBlockMappings().getMobSpawnerBlock()); + session.sendUpstreamPacket(spawnerBlockPacket); + } + } + + return super.getBlockEntityTag(session, type, x, y, z, tag, blockState); + } + @Override public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) { Tag current; @@ -69,8 +101,8 @@ public class SpawnerBlockEntityTranslator extends BlockEntityTranslator { CompoundTag spawnData = tag.get("SpawnData"); if (spawnData != null) { - StringTag idTag = ((CompoundTag) spawnData.get("entity")).get("id"); - if (idTag != null) { + CompoundTag entityTag = spawnData.get("entity"); + if (entityTag.get("id") instanceof StringTag idTag) { // As of 1.19.3, spawners can be empty String entityId = idTag.getValue(); builder.put("EntityIdentifier", entityId); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index 5beb1a201..71cb6019a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -337,7 +337,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator 1) { optionSet.add(new ItemDescriptorWithCount(new ItemTagDescriptor(recipeTag), groupedItem.count)); continue; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java index b86882d84..c67a6dee4 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEntityDataTranslator.java @@ -62,7 +62,7 @@ public class JavaBlockEntityDataTranslator extends PacketTranslator> 4)); + int subChunkIndex = sectionY + yOffset; if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) { // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client if (useExtendedCollisions) { @@ -154,7 +156,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 4) - (bedrockDimension.minY() >> 4); + int subChunkIndex = (y >> 4) + (bedrockDimension.minY() >> 4); if (0 <= bedrockSectionY && bedrockSectionY < maxBedrockSectionY) { // Custom skull is in a section accepted by Bedrock GeyserChunkSection bedrockSection = sections[bedrockSectionY]; IntList palette = bedrockSection.getBlockStorageArray()[0].getPalette(); if (palette instanceof IntImmutableList || palette instanceof IntLists.Singleton) { // TODO there has to be a better way to expand the palette .-. - bedrockSection = bedrockSection.copy(); + bedrockSection = bedrockSection.copy(subChunkIndex); sections[bedrockSectionY] = bedrockSection; } bedrockSection.setFullBlock(x, y & 0xF, z, 0, blockDefinition.getRuntimeId()); @@ -458,7 +461,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 4)); + new GeyserChunkSection(EMPTY_BLOCK_STORAGE, subChunkIndex).writeToNetwork(byteBuf); } } diff --git a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java index cded234f9..114a7b6de 100644 --- a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java @@ -54,19 +54,27 @@ import static org.geysermc.geyser.level.block.BlockStateValues.JAVA_AIR_ID; @UtilityClass public class ChunkUtils { - /** - * An empty subchunk. - */ - public static final byte[] SERIALIZED_CHUNK_DATA; + public static final byte[] EMPTY_BIOME_DATA; + public static final BlockStorage[] EMPTY_BLOCK_STORAGE; + + public static final int EMPTY_CHUNK_SECTION_SIZE; + static { + EMPTY_BLOCK_STORAGE = new BlockStorage[0]; + ByteBuf byteBuf = Unpooled.buffer(); try { - new GeyserChunkSection(new BlockStorage[0]) + new GeyserChunkSection(EMPTY_BLOCK_STORAGE, 0) .writeToNetwork(byteBuf); - SERIALIZED_CHUNK_DATA = new byte[byteBuf.readableBytes()]; - byteBuf.readBytes(SERIALIZED_CHUNK_DATA); + + byte[] emptyChunkData = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(emptyChunkData); + + EMPTY_CHUNK_SECTION_SIZE = emptyChunkData.length; + + emptyChunkData = null; } finally { byteBuf.release(); }