Feature: Structure block translation (#4521)

* ported camotoy's attempt of implementing structure blocks, removal of a few TODO's

* no more parsing of java structure templates

* Don't attempt to re-request structure size

* ensure we can load structures in even if we know the size

* init: send correct structure size/offset/rotation to java, not fully working yet

* restore offsets so we are sending correct values to the java server regarding where we want the structure to be placed

* something something mirror

* attempt at proper offsets for mirroring AND rotations. this was not fun at all

* rotation, mirror, offsetting all seem to work

* undo import changes

* fix NPE

* Proper handling of empty structures, ensure that we can clear the structure block outline when a zero vector was sent for structure size

* oops

* Update core/src/main/java/org/geysermc/geyser/session/cache/StructureBlockCache.java

Co-authored-by: rtm516 <rtm516@users.noreply.github.com>

* Update core/src/main/java/org/geysermc/geyser/translator/level/block/entity/StructureBlockBlockEntityTranslator.java

Co-authored-by: rtm516 <rtm516@users.noreply.github.com>

* Update core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockStructureBlockUpdateTranslator.java

Co-authored-by: rtm516 <rtm516@users.noreply.github.com>

* Update core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockStructureTemplateDataRequestTranslator.java

Co-authored-by: rtm516 <rtm516@users.noreply.github.com>

* consolidate java structure sending into one method

* fix merge conflict
---------

Co-authored-by: rtm516 <rtm516@users.noreply.github.com>
This commit is contained in:
chris 2024-04-17 23:21:25 +02:00 committed by GitHub
parent c8475d8100
commit 7a20a190a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 606 additions and 3 deletions

View File

@ -32,7 +32,6 @@ Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Ge
## What's Left to be Added/Fixed
- Near-perfect movement (to the point where anticheat on large servers is unlikely to ban you)
- Some Entity Flags
- Structure block UI
## What can't be fixed
There are a few things Geyser is unable to support due to various differences between Minecraft Bedrock and Java. For a list of these limitations, see the [Current Limitations](https://wiki.geysermc.org/geyser/current-limitations/) page.

View File

@ -144,7 +144,7 @@ public final class BlockRegistryPopulator {
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 //TODO verify this can be just removed
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());
@ -229,6 +229,7 @@ public final class BlockRegistryPopulator {
Map<NbtMap, BlockDefinition> itemFrames = new Object2ObjectOpenHashMap<>();
Set<BlockDefinition> jigsawDefinitions = new ObjectOpenHashSet<>();
Map<String, BlockDefinition> structureBlockDefinitions = new Object2ObjectOpenHashMap<>();
BlockMappings.BlockMappingsBuilder builder = BlockMappings.builder();
while (blocksIterator.hasNext()) {
@ -272,6 +273,18 @@ public final class BlockRegistryPopulator {
jigsawDefinitions.add(bedrockDefinition);
}
if (javaId.contains("structure_block")) {
int modeIndex = javaId.indexOf("mode=");
if (modeIndex != -1) {
int startIndex = modeIndex + 5; // Length of "mode=" is 5
int endIndex = javaId.indexOf("]", startIndex);
if (endIndex != -1) {
String modeValue = javaId.substring(startIndex, endIndex);
structureBlockDefinitions.put(modeValue.toUpperCase(), bedrockDefinition);
}
}
}
boolean waterlogged = entry.getKey().contains("waterlogged=true")
|| javaId.contains("minecraft:bubble_column") || javaId.contains("minecraft:kelp") || javaId.contains("seagrass");
@ -358,6 +371,7 @@ public final class BlockRegistryPopulator {
.itemFrames(itemFrames)
.flowerPotBlocks(flowerPotBlocks)
.jigsawStates(jigsawDefinitions)
.structureBlockStates(structureBlockDefinitions)
.remappedVanillaIds(remappedVanillaIds)
.blockProperties(customBlockProperties)
.customBlockStateDefinitions(customBlockStateDefinitions)

View File

@ -61,6 +61,7 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
Map<String, NbtMap> flowerPotBlocks;
Set<BlockDefinition> jigsawStates;
Map<String, BlockDefinition> structureBlockStates;
List<BlockPropertyData> blockProperties;
Object2ObjectMap<CustomBlockState, GeyserBedrockBlock> customBlockStateDefinitions;
@ -96,6 +97,10 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
return false;
}
public BlockDefinition getStructureBlockFromMode(String mode) {
return structureBlockStates.get(mode);
}
@Override
public @Nullable GeyserBedrockBlock getDefinition(int bedrockId) {
if (bedrockId < 0 || bedrockId >= this.bedrockRuntimeMap.length) {

View File

@ -214,6 +214,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private final PistonCache pistonCache;
private final PreferencesCache preferencesCache;
private final SkullCache skullCache;
private final StructureBlockCache structureBlockCache;
private final TagCache tagCache;
private final WorldCache worldCache;
@ -625,6 +626,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.pistonCache = new PistonCache(this);
this.preferencesCache = new PreferencesCache(this);
this.skullCache = new SkullCache(this);
this.structureBlockCache = new StructureBlockCache();
this.tagCache = new TagCache();
this.worldCache = new WorldCache(this);
this.cameraData = new GeyserCameraData(this);

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2019-2024 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.session.cache;
import lombok.Getter;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3i;
@Setter
@Getter
public final class StructureBlockCache {
/**
* Stores the current structure's name to be able to detect changes in the loaded structure
*/
private @Nullable String currentStructureName;
/**
* Stores the offset changes added by Geyser that ensure that structure bounds
* are the same for Java and Bedrock
*/
private @Nullable Vector3i bedrockOffset;
/**
* Stores the current structure block position while we're waiting on the Java
* server to send the data we need.
*/
private @Nullable Vector3i currentStructureBlock;
public void clear() {
this.currentStructureName = null;
this.currentStructureBlock = null;
this.bedrockOffset = null;
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2019-2024 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.translator.level.block.entity;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureMirror;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureRotation;
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.StructureBlockUtils;
@BlockEntity(type = BlockEntityType.STRUCTURE_BLOCK)
public class StructureBlockBlockEntityTranslator extends BlockEntityTranslator {
@Override
public NbtMap getBlockEntityTag(GeyserSession session, BlockEntityType type, int x, int y, int z, CompoundTag tag, int blockState) {
// Sending a structure with size 0 doesn't clear the outline. Hence, we have to force it by replacing the block :/
int xStructureSize = getOrDefault(tag.get("sizeX"), 0);
int yStructureSize = getOrDefault(tag.get("sizeY"), 0);
int zStructureSize = getOrDefault(tag.get("sizeZ"), 0);
Vector3i size = Vector3i.from(xStructureSize, yStructureSize, zStructureSize);
if (size.equals(Vector3i.ZERO)) {
Vector3i position = Vector3i.from(x, y, z);
String mode = getOrDefault(tag.get("mode"), "");
// Set to air and back to reset the structure block
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().getStructureBlockFromMode(mode));
session.sendUpstreamPacket(spawnerBlockPacket);
}
return super.getBlockEntityTag(session, type, x, y, z, tag, blockState);
}
@Override
public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) {
if (tag.size() < 5) {
return; // These values aren't here
}
builder.putString("structureName", getOrDefault(tag.get("name"), ""));
String mode = getOrDefault(tag.get("mode"), "");
int bedrockData = switch (mode) {
case "LOAD" -> 2;
case "CORNER" -> 3;
case "DATA" -> 4;
default -> 1; // SAVE
};
builder.putInt("data", bedrockData);
builder.putString("dataField", ""); // ??? possibly related to Java's "metadata"
// Mirror behaves different in Java and Bedrock - it requires modifying the position in space as well
String mirror = getOrDefault(tag.get("mirror"), "");
StructureMirror bedrockMirror = switch (mirror) {
case "FRONT_BACK" -> StructureMirror.X;
case "LEFT_RIGHT" -> StructureMirror.Z;
default -> StructureMirror.NONE;
};
builder.putByte("mirror", (byte) bedrockMirror.ordinal());
builder.putByte("ignoreEntities", getOrDefault(tag.get("ignoreEntities"), (byte) 0));
builder.putByte("isPowered", getOrDefault(tag.get("powered"), (byte) 0));
builder.putLong("seed", getOrDefault(tag.get("seed"), 0L));
builder.putByte("showBoundingBox", getOrDefault(tag.get("showboundingbox"), (byte) 0));
String rotation = getOrDefault(tag.get("rotation"), "");
StructureRotation bedrockRotation = switch (rotation) {
case "CLOCKWISE_90" -> StructureRotation.ROTATE_90;
case "CLOCKWISE_180" -> StructureRotation.ROTATE_180;
case "COUNTERCLOCKWISE_90" -> StructureRotation.ROTATE_270;
default -> StructureRotation.NONE;
};
builder.putByte("rotation", (byte) bedrockRotation.ordinal());
int xStructureSize = getOrDefault(tag.get("sizeX"), 0);
int yStructureSize = getOrDefault(tag.get("sizeY"), 0);
int zStructureSize = getOrDefault(tag.get("sizeZ"), 0);
// The "positions" are also offsets on Java
int posX = getOrDefault(tag.get("posX"), 0);
int posY = getOrDefault(tag.get("posY"), 0);
int posZ = getOrDefault(tag.get("posZ"), 0);
Vector3i offset = StructureBlockUtils.calculateOffset(bedrockRotation, bedrockMirror,
xStructureSize, zStructureSize);
builder.putInt("xStructureOffset", posX + offset.getX());
builder.putInt("yStructureOffset", posY);
builder.putInt("zStructureOffset", posZ + offset.getZ());
builder.putInt("xStructureSize", xStructureSize);
builder.putInt("yStructureSize", yStructureSize);
builder.putInt("zStructureSize", zStructureSize);
builder.putFloat("integrity", getOrDefault(tag.get("integrity"), 0f)); // Is 1.0f by default on Java but 100.0f on Bedrock
// Java's "showair" is unrepresented
}
}

View File

@ -348,6 +348,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
openPacket.setType(ContainerType.JIGSAW_EDITOR);
openPacket.setUniqueEntityId(-1);
session.sendUpstreamPacket(openPacket);
} else if (session.getBlockMappings().getStructureBlockStates().containsValue(packet.getBlockDefinition())) {
ContainerOpenPacket openPacket = new ContainerOpenPacket();
openPacket.setBlockPosition(packet.getBlockPosition());
openPacket.setId((byte) 1);
openPacket.setType(ContainerType.STRUCTURE_EDITOR);
openPacket.setUniqueEntityId(-1);
session.sendUpstreamPacket(openPacket);
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2019-2024 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.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.inventory.UpdateStructureBlockAction;
import com.github.steveice10.mc.protocol.data.game.inventory.UpdateStructureBlockMode;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureBlockType;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureEditorData;
import org.cloudburstmc.protocol.bedrock.packet.StructureBlockUpdatePacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.StructureBlockUtils;
@Translator(packet = StructureBlockUpdatePacket.class)
public class BedrockStructureBlockUpdateTranslator extends PacketTranslator<StructureBlockUpdatePacket> {
@Override
public void translate(GeyserSession session, StructureBlockUpdatePacket packet) {
StructureEditorData data = packet.getEditorData();
UpdateStructureBlockAction action = UpdateStructureBlockAction.UPDATE_DATA;
if (packet.isPowered()) {
if (data.getType() == StructureBlockType.LOAD) {
action = UpdateStructureBlockAction.LOAD_STRUCTURE;
} else if (data.getType() == StructureBlockType.SAVE) {
action = UpdateStructureBlockAction.SAVE_STRUCTURE;
}
}
UpdateStructureBlockMode mode = switch (data.getType()) {
case CORNER -> UpdateStructureBlockMode.CORNER;
case DATA -> UpdateStructureBlockMode.DATA;
case LOAD -> UpdateStructureBlockMode.LOAD;
default -> UpdateStructureBlockMode.SAVE;
};
StructureBlockUtils.sendJavaStructurePacket(session, packet.getBlockPosition(), data.getSettings().getSize(), mode, action, data.getSettings(),
data.isBoundingBoxVisible(), data.getName());
session.getStructureBlockCache().clear();
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2019-2024 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.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.inventory.UpdateStructureBlockAction;
import com.github.steveice10.mc.protocol.data.game.inventory.UpdateStructureBlockMode;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureSettings;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureTemplateRequestOperation;
import org.cloudburstmc.protocol.bedrock.packet.StructureTemplateDataRequestPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.StructureBlockUtils;
/**
* Packet used in Bedrock to load structure size into the structure block GUI. It is sent every time the GUI is opened.
* Or, if the player updates the structure name. Which we can use to request the structure size from the Java server!
* <p>
* Java does not have this preview, instead, Java clients are forced out of the GUI to look at the area.
*/
@Translator(packet = StructureTemplateDataRequestPacket.class)
public class BedrockStructureTemplateDataRequestTranslator extends PacketTranslator<StructureTemplateDataRequestPacket> {
@Override
public void translate(GeyserSession session, StructureTemplateDataRequestPacket packet) {
// All other operation types are ignored by Geyser since we do not support exporting/importing structures
if (packet.getOperation().equals(StructureTemplateRequestOperation.QUERY_SAVED_STRUCTURE)) {
Vector3i size = packet.getSettings().getSize();
StructureSettings settings = packet.getSettings();
// If we send a load packet to the Java server when the structure size is known, it would place the structure.
String currentStructureName = session.getStructureBlockCache().getCurrentStructureName();
// Case 1: Opening a structure block with information about structure size, but not yet saved by us
// Case 2: Getting an update from Bedrock with new information, doesn't bother us if it's the same structure
if (!packet.getSettings().getSize().equals(Vector3i.ZERO)) {
if (currentStructureName == null) {
Vector3i offset = StructureBlockUtils.calculateOffset(settings.getRotation(), settings.getMirror(),
settings.getSize().getX(), settings.getSize().getZ());
session.getStructureBlockCache().setBedrockOffset(offset);
session.getStructureBlockCache().setCurrentStructureName(packet.getName());
StructureBlockUtils.sendStructureData(session, size, packet.getName());
return;
} else if (packet.getName().equals(currentStructureName)) {
StructureBlockUtils.sendStructureData(session, size, packet.getName());
return;
}
}
// Request a "structure load" from Java server, so it sends us the structure's size
// See the block entity translator for more info
session.getStructureBlockCache().setCurrentStructureBlock(packet.getPosition());
StructureBlockUtils.sendJavaStructurePacket(session,
packet.getPosition(),
Vector3i.ZERO, // We expect the Java server to tell us the size
UpdateStructureBlockMode.LOAD,
UpdateStructureBlockAction.LOAD_STRUCTURE,
settings,
true,
packet.getName()
);
} else {
StructureBlockUtils.sendEmptyStructureData(session);
}
}
}

View File

@ -28,9 +28,13 @@ package org.geysermc.geyser.translator.protocol.java.level;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundBlockEntityDataPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureMirror;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureRotation;
import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
import org.geysermc.geyser.level.block.BlockStateValues;
@ -41,6 +45,7 @@ import org.geysermc.geyser.translator.level.block.entity.SkullBlockEntityTransla
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.geysermc.geyser.util.StructureBlockUtils;
@Translator(packet = ClientboundBlockEntityDataPacket.class)
public class JavaBlockEntityDataTranslator extends PacketTranslator<ClientboundBlockEntityDataPacket> {
@ -95,5 +100,59 @@ public class JavaBlockEntityDataTranslator extends PacketTranslator<ClientboundB
openPacket.setUniqueEntityId(-1);
session.sendUpstreamPacket(openPacket);
}
// When a Java client is trying to load a structure, it expects the server to send it the size of the structure.
// On 1.20.4, the server does so here - we can pass that through to Bedrock, so we're properly selecting the area.
if (type == BlockEntityType.STRUCTURE_BLOCK && session.getGameMode() == GameMode.CREATIVE
&& packet.getPosition().equals(session.getStructureBlockCache().getCurrentStructureBlock())
&& packet.getNbt() != null && packet.getNbt().size() > 5
) {
CompoundTag map = packet.getNbt();
String mode = getOrDefault(map.get("mode"), "");
if (!mode.equalsIgnoreCase("LOAD")) {
return;
}
String mirror = getOrDefault(map.get("mirror"), "");
StructureMirror bedrockMirror = switch (mirror) {
case "FRONT_BACK" -> StructureMirror.X;
case "LEFT_RIGHT" -> StructureMirror.Z;
default -> StructureMirror.NONE;
};
String rotation = getOrDefault(map.get("rotation"), "");
StructureRotation bedrockRotation = switch (rotation) {
case "CLOCKWISE_90" -> StructureRotation.ROTATE_90;
case "CLOCKWISE_180" -> StructureRotation.ROTATE_180;
case "COUNTERCLOCKWISE_90" -> StructureRotation.ROTATE_270;
default -> StructureRotation.NONE;
};
String name = getOrDefault(map.get("name"), "");
int sizeX = getOrDefault(map.get("sizeX"), 0);
int sizeY = getOrDefault(map.get("sizeY"), 0);
int sizeZ = getOrDefault(map.get("sizeZ"), 0);
session.getStructureBlockCache().setCurrentStructureBlock(null);
Vector3i size = Vector3i.from(sizeX, sizeY, sizeZ);
if (size.equals(Vector3i.ZERO)) {
StructureBlockUtils.sendEmptyStructureData(session);
return;
}
Vector3i offset = StructureBlockUtils.calculateOffset(bedrockRotation, bedrockMirror,
sizeX, sizeZ);
session.getStructureBlockCache().setBedrockOffset(offset);
session.getStructureBlockCache().setCurrentStructureName(name);
StructureBlockUtils.sendStructureData(session, size, name);
}
}
protected <T> T getOrDefault(Tag tag, T defaultValue) {
//noinspection unchecked
return (tag != null && tag.getValue() != null) ? (T) tag.getValue() : defaultValue;
}
}

View File

@ -125,7 +125,7 @@ public class InventoryUtils {
InventoryTranslator translator = session.getInventoryTranslator();
translator.closeInventory(session, inventory);
if (confirm && inventory.isDisplayed() && !inventory.isPending()
&& !(translator instanceof LecternInventoryTranslator) // TODO: double-check
&& !(translator instanceof LecternInventoryTranslator) // Closing lecterns is not followed with a close confirmation
) {
session.setClosingInventory(true);
}

View File

@ -0,0 +1,164 @@
/*
* Copyright (c) 2019-2024 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 com.github.steveice10.mc.protocol.data.game.inventory.UpdateStructureBlockAction;
import com.github.steveice10.mc.protocol.data.game.inventory.UpdateStructureBlockMode;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetStructureBlockPacket;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtList;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureMirror;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureRotation;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureSettings;
import org.cloudburstmc.protocol.bedrock.data.structure.StructureTemplateResponseType;
import org.cloudburstmc.protocol.bedrock.packet.StructureTemplateDataResponsePacket;
import org.geysermc.geyser.session.GeyserSession;
public class StructureBlockUtils {
private static final NbtMap EMPTY_STRUCTURE_DATA;
static {
NbtMapBuilder builder = NbtMap.builder();
builder.putInt("format_version", 1);
builder.putCompound("structure", NbtMap.builder()
.putList("block_indices", NbtType.LIST, NbtList.EMPTY, NbtList.EMPTY)
.putList("entities", NbtType.COMPOUND)
.putCompound("palette", NbtMap.EMPTY)
.build());
builder.putList("structure_world_origin", NbtType.INT, 0, 0, 0);
EMPTY_STRUCTURE_DATA = builder.build();
}
public static void sendEmptyStructureData(GeyserSession session) {
StructureTemplateDataResponsePacket responsePacket = new StructureTemplateDataResponsePacket();
responsePacket.setName("");
responsePacket.setSave(false);
responsePacket.setType(StructureTemplateResponseType.QUERY);
session.sendUpstreamPacket(responsePacket);
}
public static void sendStructureData(GeyserSession session,Vector3i size, String name) {
StructureTemplateDataResponsePacket responsePacket = new StructureTemplateDataResponsePacket();
responsePacket.setName(name);
responsePacket.setSave(true);
responsePacket.setTag(EMPTY_STRUCTURE_DATA.toBuilder()
// Bedrock does not like negative sizes here
.putList("size", NbtType.INT, Math.abs(size.getX()), size.getY(), Math.abs(size.getZ()))
.build());
responsePacket.setType(StructureTemplateResponseType.QUERY);
session.sendUpstreamPacket(responsePacket);
}
public static Vector3i calculateOffset(StructureRotation structureRotation, StructureMirror structureMirror,
int sizeX, int sizeZ) {
int newOffsetX = 0;
int newOffsetZ = 0;
switch (structureRotation) {
case ROTATE_90 -> {
switch (structureMirror) {
case NONE -> newOffsetX -= sizeZ - 1;
case X -> {
newOffsetZ -= sizeX - 1;
newOffsetX -= sizeZ - 1;
}
}
}
case ROTATE_180 -> {
switch (structureMirror) {
case NONE -> {
newOffsetX -= sizeX - 1;
newOffsetZ -= sizeZ - 1;
}
case Z -> newOffsetX -= sizeX - 1;
case X -> newOffsetZ -= sizeZ - 1;
}
}
case ROTATE_270 -> {
switch (structureMirror) {
case NONE -> newOffsetZ -= sizeX - 1;
case Z -> {
newOffsetZ -= sizeX - 1;
newOffsetX -= sizeZ - 1;
}
}
}
default -> {
switch (structureMirror) {
case Z -> newOffsetZ -= sizeZ - 1;
case X -> newOffsetX -= sizeX - 1;
}
}
}
return Vector3i.from(newOffsetX, 0, newOffsetZ);
}
public static void sendJavaStructurePacket(GeyserSession session, Vector3i blockPosition, Vector3i size, UpdateStructureBlockMode mode, UpdateStructureBlockAction action,
StructureSettings settings, boolean boundingBoxVisible, String structureName) {
com.github.steveice10.mc.protocol.data.game.level.block.StructureMirror mirror = switch (settings.getMirror()) {
case X -> com.github.steveice10.mc.protocol.data.game.level.block.StructureMirror.FRONT_BACK;
case Z -> com.github.steveice10.mc.protocol.data.game.level.block.StructureMirror.LEFT_RIGHT;
default -> com.github.steveice10.mc.protocol.data.game.level.block.StructureMirror.NONE;
};
com.github.steveice10.mc.protocol.data.game.level.block.StructureRotation rotation = switch (settings.getRotation()) {
case ROTATE_90 -> com.github.steveice10.mc.protocol.data.game.level.block.StructureRotation.CLOCKWISE_90;
case ROTATE_180 -> com.github.steveice10.mc.protocol.data.game.level.block.StructureRotation.CLOCKWISE_180;
case ROTATE_270 -> com.github.steveice10.mc.protocol.data.game.level.block.StructureRotation.COUNTERCLOCKWISE_90;
default -> com.github.steveice10.mc.protocol.data.game.level.block.StructureRotation.NONE;
};
Vector3i offset = settings.getOffset();
if (session.getStructureBlockCache().getBedrockOffset() != null) {
offset = settings.getOffset().sub(session.getStructureBlockCache().getBedrockOffset());
}
ServerboundSetStructureBlockPacket structureBlockPacket = new ServerboundSetStructureBlockPacket(
blockPosition,
action,
mode,
structureName,
offset,
settings.getSize(),
mirror,
rotation,
"",
settings.getIntegrityValue(),
settings.getIntegritySeed(),
settings.isIgnoringEntities(),
false,
boundingBoxVisible
);
session.sendDownstreamPacket(structureBlockPacket);
}
}