Merge branch 'master' of https://github.com/GeyserMC/Geyser into feature/extensions

This commit is contained in:
Camotoy 2022-02-10 09:17:27 -05:00
commit 354e87b747
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
77 changed files with 2001 additions and 1476 deletions

View file

@ -17,7 +17,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
### Currently supporting Minecraft Bedrock 1.17.30 - 1.17.41 + 1.18.0 - 1.18.2 and Minecraft Java 1.18/1.18.1.
### Currently supporting Minecraft Bedrock 1.17.41 + 1.18.0 - 1.18.10 and Minecraft Java 1.18/1.18.1.
## Setting Up
Take a look [here](https://github.com/GeyserMC/Geyser/wiki/Setup) for how to set up Geyser.

View file

@ -6,9 +6,9 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>ap</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</project>

View file

@ -5,7 +5,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>api-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View file

@ -5,7 +5,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>api-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -35,7 +35,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>base-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-bungeecord</artifactId>
@ -14,7 +14,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>core</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- Used for better working with internals without reflection -->

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-parent</artifactId>
<packaging>pom</packaging>
@ -34,7 +34,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>ap</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-spigot</artifactId>
@ -25,7 +25,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>core</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View file

@ -97,7 +97,7 @@ public class GeyserPistonListener implements Listener {
int dX = Math.abs(location.getBlockX() - player.getLocation().getBlockX()) >> 4;
int dZ = Math.abs(location.getBlockZ() - player.getLocation().getBlockZ()) >> 4;
if ((dX * dX + dZ * dZ) > session.getRenderDistance() * session.getRenderDistance()) {
if ((dX * dX + dZ * dZ) > session.getServerRenderDistance() * session.getServerRenderDistance()) {
// Ignore pistons outside the player's render distance
continue;
}

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-sponge</artifactId>
@ -14,7 +14,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>core</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-standalone</artifactId>
@ -18,7 +18,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>core</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-velocity</artifactId>
@ -14,7 +14,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>core</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>common</artifactId>

View file

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
</parent>
<artifactId>core</artifactId>
@ -20,19 +20,19 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>ap</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>geyser-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>common</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- Jackson JSON and YAML serialization -->
@ -119,8 +119,8 @@
</dependency>
<dependency>
<groupId>com.github.CloudburstMC.Protocol</groupId>
<artifactId>bedrock-v475</artifactId>
<version>c22aa595</version>
<artifactId>bedrock-v486</artifactId>
<version>0cd24c0</version>
<scope>compile</scope>
<exclusions>
<exclusion>

View file

@ -64,7 +64,7 @@ public class GeyserSession {
}
public int getRenderDistance() {
return this.handle.getRenderDistance();
return this.handle.getServerRenderDistance();
}
public boolean isSentSpawnPacket() {

View file

@ -68,6 +68,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.SessionManager;
import org.geysermc.geyser.session.auth.AuthType;
import org.geysermc.geyser.skin.FloodgateSkinUploader;
import org.geysermc.geyser.skin.SkinProvider;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
@ -211,6 +212,8 @@ public class GeyserImpl implements GeyserApi {
ScoreboardUpdater.init();
SkinProvider.registerCacheImageTask(this);
ResourcePack.loadPacks();
this.extensionManager.enableExtensions();

View file

@ -25,6 +25,8 @@
package org.geysermc.geyser;
import javax.annotation.Nullable;
public interface GeyserLogger {
/**
@ -78,6 +80,15 @@ public interface GeyserLogger {
*/
void debug(String message);
/**
* Logs an object to console if debug mode is enabled
*
* @param object the object to log
*/
default void debug(@Nullable Object object) {
debug(String.valueOf(object));
}
/**
* Sets if the logger should print debug messages
*

View file

@ -76,6 +76,10 @@ public interface GeyserConfiguration {
boolean isShowCoordinates();
boolean isDisableBedrockScaffolding();
boolean isAlwaysQuickChangeArmor();
EmoteOffhandWorkaroundOption getEmoteOffhandWorkaround();
String getDefaultLocale();

View file

@ -105,6 +105,12 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("show-coordinates")
private boolean showCoordinates = true;
@JsonProperty("disable-bedrock-scaffolding")
private boolean disableBedrockScaffolding = false;
@JsonProperty("always-quick-change-armor")
private boolean alwaysQuickChangeArmor = false;
@JsonDeserialize(using = EmoteOffhandWorkaroundOption.Deserializer.class)
@JsonProperty("emote-offhand-workaround")
private EmoteOffhandWorkaroundOption emoteOffhandWorkaround = EmoteOffhandWorkaroundOption.DISABLED;

View file

@ -34,7 +34,7 @@ import java.util.Map;
/**
* A write-only wrapper for temporarily storing entity metadata that will be sent to Bedrock.
*/
public class GeyserDirtyMetadata {
public final class GeyserDirtyMetadata {
private final Map<EntityData, Object> metadata = new Object2ObjectLinkedOpenHashMap<>();
public void put(EntityData entityData, Object value) {

View file

@ -37,12 +37,11 @@ import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
import lombok.Getter;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.geysermc.geyser.registry.type.ItemMapping;
import java.util.UUID;
@ -85,10 +84,8 @@ public class ItemFrameEntity extends Entity {
.putInt("version", session.getBlockMappings().getBlockStateVersion());
NbtMapBuilder statesBuilder = NbtMap.builder()
.putInt("facing_direction", direction.ordinal())
.putByte("item_frame_map_bit", (byte) 0);
if (session.getUpstream().getProtocolVersion() >= Bedrock_v465.V465_CODEC.getProtocolVersion()) {
statesBuilder.putByte("item_frame_photo_bit", (byte) 0);
}
.putByte("item_frame_map_bit", (byte) 0)
.putByte("item_frame_photo_bit", (byte) 0);
blockBuilder.put("states", statesBuilder.build());
bedrockRuntimeId = session.getBlockMappings().getItemFrame(blockBuilder.build());

View file

@ -136,7 +136,7 @@ public class ArmorStandEntity extends LivingEntity {
}
isSmall = newIsSmall;
if (!isMarker) {
if (!isMarker && !isInvisible) { // Addition for isInvisible check caused by https://github.com/GeyserMC/Geyser/issues/2780
toggleSmallStatus();
}
}

View file

@ -382,15 +382,26 @@ public class PlayerEntity extends LivingEntity {
@Override
protected void setDimensions(Pose pose) {
float height;
float width;
switch (pose) {
case SNEAKING -> height = SNEAKING_POSE_HEIGHT;
case FALL_FLYING, SPIN_ATTACK, SWIMMING -> height = 0.6f;
case SNEAKING -> {
height = SNEAKING_POSE_HEIGHT;
width = definition.width();
}
case FALL_FLYING, SPIN_ATTACK, SWIMMING -> {
height = 0.6f;
width = definition.width();
}
case DYING -> {
height = 0.2f;
width = 0.2f;
}
default -> {
super.setDimensions(pose);
return;
}
}
setBoundingBoxWidth(definition.width());
setBoundingBoxWidth(width);
setBoundingBoxHeight(height);
}

View file

@ -26,10 +26,6 @@
package org.geysermc.geyser.inventory;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
import lombok.Value;
@Value
public class BedrockContainerSlot {
ContainerSlotType container;
int slot;
public record BedrockContainerSlot(ContainerSlotType container, int slot) {
}

View file

@ -27,9 +27,11 @@ package org.geysermc.geyser.inventory;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import lombok.Getter;
import lombok.NonNull;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.jetbrains.annotations.Range;
import javax.annotation.Nonnull;
/**
* Combination of {@link Inventory} and {@link PlayerInventory}
@ -60,7 +62,12 @@ public class Container extends Inventory {
}
@Override
public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) {
public int getOffsetForHotbar(@Range(from = 0, to = 8) int slot) {
return playerInventory.getOffsetForHotbar(slot) - InventoryTranslator.PLAYER_INVENTORY_OFFSET + this.size;
}
@Override
public void setItem(int slot, @Nonnull GeyserItemStack newItem, GeyserSession session) {
if (slot < this.size) {
super.setItem(slot, newItem, session);
} else {

View file

@ -31,17 +31,19 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.math.vector.Vector3i;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.jetbrains.annotations.Range;
import javax.annotation.Nonnull;
import java.util.Arrays;
@ToString
public class Inventory {
public abstract class Inventory {
@Getter
protected final int id;
@ -69,8 +71,7 @@ public class Inventory {
protected final ContainerType containerType;
@Getter
@Setter
protected String title;
protected final String title;
protected final GeyserItemStack[] items;
@ -110,7 +111,9 @@ public class Inventory {
return items[slot];
}
public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) {
public abstract int getOffsetForHotbar(@Range(from = 0, to = 8) int slot);
public void setItem(int slot, @Nonnull GeyserItemStack newItem, GeyserSession session) {
if (slot > this.size) {
session.getGeyser().getLogger().debug("Tried to set an item out of bounds! " + this);
return;
@ -133,7 +136,9 @@ public class Inventory {
protected void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) {
if (!newItem.isEmpty()) {
if (newItem.getItemData(session).equals(oldItem.getItemData(session), false, false, false)) {
ItemMapping oldMapping = ItemTranslator.getBedrockItemMapping(session, oldItem);
ItemMapping newMapping = ItemTranslator.getBedrockItemMapping(session, newItem);
if (oldMapping.getBedrockId() == newMapping.getBedrockId()) {
newItem.setNetId(oldItem.getNetId());
} else {
newItem.setNetId(session.getNextItemNetId());

View file

@ -26,10 +26,12 @@
package org.geysermc.geyser.inventory;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.GeyserSession;
import org.jetbrains.annotations.Range;
import javax.annotation.Nonnull;
public class PlayerInventory extends Inventory {
/**
@ -41,7 +43,7 @@ public class PlayerInventory extends Inventory {
private int heldItemSlot;
@Getter
@NonNull
@Nonnull
private GeyserItemStack cursor = GeyserItemStack.EMPTY;
public PlayerInventory() {
@ -49,7 +51,12 @@ public class PlayerInventory extends Inventory {
heldItemSlot = 0;
}
public void setCursor(@NonNull GeyserItemStack newCursor, GeyserSession session) {
@Override
public int getOffsetForHotbar(@Range(from = 0, to = 8) int slot) {
return slot + 36;
}
public void setCursor(@Nonnull GeyserItemStack newCursor, GeyserSession session) {
updateItemNetId(cursor, newCursor, session);
cursor = newCursor;
}
@ -62,7 +69,7 @@ public class PlayerInventory extends Inventory {
return items[36 + heldItemSlot];
}
public void setItemInHand(@NonNull GeyserItemStack item) {
public void setItemInHand(@Nonnull GeyserItemStack item) {
if (36 + heldItemSlot > this.size) {
GeyserImpl.getInstance().getLogger().debug("Held item slot was larger than expected!");
return;

View file

@ -40,20 +40,22 @@ import org.geysermc.geyser.inventory.SlotType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator;
import org.geysermc.geyser.util.InventoryUtils;
import org.jetbrains.annotations.Contract;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
public class ClickPlan {
public final class ClickPlan {
private final List<ClickAction> plan = new ArrayList<>();
private final Int2ObjectMap<GeyserItemStack> simulatedItems;
/**
* Used for 1.17.1+ proper packet translation - any non-cursor item that is changed in a single transaction gets sent here.
*/
private Int2ObjectMap<ItemStack> changedItems;
private GeyserItemStack simulatedCursor;
private boolean simulating;
private boolean finished;
private final GeyserSession session;
private final InventoryTranslator translator;
@ -66,16 +68,11 @@ public class ClickPlan {
this.inventory = inventory;
this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
this.changedItems = null;
this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
this.simulating = true;
this.finished = false;
if (translator instanceof PlayerInventoryTranslator) {
gridSize = 4;
} else if (translator instanceof CraftingInventoryTranslator) {
gridSize = 9;
} else {
gridSize = -1;
}
gridSize = translator.getGridSize();
}
private void resetSimulation() {
@ -88,7 +85,7 @@ public class ClickPlan {
}
public void add(Click click, int slot, boolean force) {
if (!simulating)
if (finished)
throw new UnsupportedOperationException("ClickPlan already executed");
if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) {
@ -97,6 +94,8 @@ public class ClickPlan {
ClickAction action = new ClickAction(click, slot, force);
plan.add(action);
// RUNNING THE SIMULATION HERE IS IMPORTANT. The contents of the simulation are used in complex, multi-stage tasks
// such as autocrafting.
simulateAction(action);
}
@ -112,33 +111,48 @@ public class ClickPlan {
refresh = true;
}
//int stateId = stateIdHack(action);
changedItems = new Int2ObjectOpenHashMap<>();
//simulateAction(action);
boolean emulatePost1_16Logic = session.isEmulatePost1_16Logic();
int stateId;
if (emulatePost1_16Logic) {
stateId = stateIdHack(action);
simulateAction(action);
} else {
stateId = inventory.getStateId();
}
ItemStack clickedItemStack;
if (!planIter.hasNext() && refresh) {
clickedItemStack = InventoryUtils.REFRESH_ITEM;
} else if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) {
clickedItemStack = null;
} else {
//// The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1)
//clickedItemStack = simulatedCursor.getItemStack(); TODO fix - this is the proper behavior but it terribly breaks 1.16.5
clickedItemStack = getItem(action.slot).getItemStack();
if (emulatePost1_16Logic) {
// The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1)
clickedItemStack = simulatedCursor.getItemStack();
} else {
if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) {
clickedItemStack = null;
} else {
clickedItemStack = getItem(action.slot).getItemStack();
}
}
}
if (!emulatePost1_16Logic) {
simulateAction(action);
}
ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket(
inventory.getId(),
inventory.getStateId(),
stateId,
action.slot,
action.click.actionType,
action.click.action,
clickedItemStack,
Collections.emptyMap() // Anything else we change, at this time, should have a packet sent to address
changedItems
);
simulateAction(action);
session.sendDownstreamPacket(clickPacket);
}
@ -146,19 +160,11 @@ public class ClickPlan {
for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
}
simulating = false;
finished = true;
}
public GeyserItemStack getItem(int slot) {
return getItem(slot, true);
}
public GeyserItemStack getItem(int slot, boolean generate) {
if (generate) {
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
} else {
return simulatedItems.getOrDefault(slot, inventory.getItem(slot));
}
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
}
public GeyserItemStack getCursor() {
@ -166,23 +172,40 @@ public class ClickPlan {
}
private void setItem(int slot, GeyserItemStack item) {
if (simulating) {
simulatedItems.put(slot, item);
} else {
inventory.setItem(slot, item, session);
}
simulatedItems.put(slot, item);
onSlotItemChange(slot, item);
}
private void setCursor(GeyserItemStack item) {
if (simulating) {
simulatedCursor = item;
} else {
session.getPlayerInventory().setCursor(item, session);
simulatedCursor = item;
}
private void add(int slot, GeyserItemStack itemStack, int amount) {
itemStack.add(amount);
onSlotItemChange(slot, itemStack);
}
private void sub(int slot, GeyserItemStack itemStack, int amount) {
itemStack.sub(amount);
onSlotItemChange(slot, itemStack);
}
private void setAmount(int slot, GeyserItemStack itemStack, int amount) {
itemStack.setAmount(amount);
onSlotItemChange(slot, itemStack);
}
/**
* Does not need to be called for the cursor
*/
private void onSlotItemChange(int slot, GeyserItemStack itemStack) {
if (changedItems != null) {
changedItems.put(slot, itemStack.getItemStack());
}
}
private void simulateAction(ClickAction action) {
GeyserItemStack cursor = simulating ? getCursor() : session.getPlayerInventory().getCursor();
GeyserItemStack cursor = getCursor();
switch (action.click) {
case LEFT_OUTSIDE -> {
setCursor(GeyserItemStack.EMPTY);
@ -196,7 +219,7 @@ public class ClickPlan {
}
}
GeyserItemStack clicked = simulating ? getItem(action.slot) : inventory.getItem(action.slot);
GeyserItemStack clicked = getItem(action.slot);
if (translator.getSlotType(action.slot) == SlotType.OUTPUT) {
switch (action.click) {
case LEFT, RIGHT -> {
@ -206,6 +229,7 @@ public class ClickPlan {
cursor.add(clicked.getAmount());
}
reduceCraftingGrid(false);
setItem(action.slot, GeyserItemStack.EMPTY); // Matches Java Edition 1.18.1
}
case LEFT_SHIFT -> reduceCraftingGrid(true);
}
@ -217,55 +241,55 @@ public class ClickPlan {
setItem(action.slot, cursor);
} else {
setCursor(GeyserItemStack.EMPTY);
clicked.add(cursor.getAmount());
add(action.slot, clicked, cursor.getAmount());
}
break;
case RIGHT:
if (cursor.isEmpty() && !clicked.isEmpty()) {
int half = clicked.getAmount() / 2; //smaller half
setCursor(clicked.copy(clicked.getAmount() - half)); //larger half
clicked.setAmount(half);
setAmount(action.slot, clicked, half);
} else if (!cursor.isEmpty() && clicked.isEmpty()) {
cursor.sub(1);
setItem(action.slot, cursor.copy(1));
} else if (InventoryUtils.canStack(cursor, clicked)) {
cursor.sub(1);
clicked.add(1);
add(action.slot, clicked, 1);
}
break;
case SWAP_TO_HOTBAR_1:
swap(action.slot, 36, clicked);
swap(action.slot, inventory.getOffsetForHotbar(0), clicked);
break;
case SWAP_TO_HOTBAR_2:
swap(action.slot, 37, clicked);
swap(action.slot, inventory.getOffsetForHotbar(1), clicked);
break;
case SWAP_TO_HOTBAR_3:
swap(action.slot, 38, clicked);
swap(action.slot, inventory.getOffsetForHotbar(2), clicked);
break;
case SWAP_TO_HOTBAR_4:
swap(action.slot, 39, clicked);
swap(action.slot, inventory.getOffsetForHotbar(3), clicked);
break;
case SWAP_TO_HOTBAR_5:
swap(action.slot, 40, clicked);
swap(action.slot, inventory.getOffsetForHotbar(4), clicked);
break;
case SWAP_TO_HOTBAR_6:
swap(action.slot, 41, clicked);
swap(action.slot, inventory.getOffsetForHotbar(5), clicked);
break;
case SWAP_TO_HOTBAR_7:
swap(action.slot, 42, clicked);
swap(action.slot, inventory.getOffsetForHotbar(6), clicked);
break;
case SWAP_TO_HOTBAR_8:
swap(action.slot, 43, clicked);
swap(action.slot, inventory.getOffsetForHotbar(7), clicked);
break;
case SWAP_TO_HOTBAR_9:
swap(action.slot, 44, clicked);
swap(action.slot, inventory.getOffsetForHotbar(8), clicked);
break;
case LEFT_SHIFT:
//TODO
break;
case DROP_ONE:
if (!clicked.isEmpty()) {
clicked.sub(1);
sub(action.slot, clicked, 1);
}
break;
case DROP_ALL:
@ -279,7 +303,7 @@ public class ClickPlan {
* Swap between two inventory slots without a cursor. This should only be used with {@link ContainerActionType#MOVE_TO_HOTBAR_SLOT}
*/
private void swap(int sourceSlot, int destSlot, GeyserItemStack sourceItem) {
GeyserItemStack destinationItem = simulating ? getItem(destSlot) : inventory.getItem(destSlot);
GeyserItemStack destinationItem = getItem(destSlot);
setItem(sourceSlot, destinationItem);
setItem(destSlot, sourceItem);
}
@ -292,63 +316,44 @@ public class ClickPlan {
stateId = inventory.getStateId();
}
// This is a hack.
// Java will never ever send more than one container click packet per set of actions.
// Java will never ever send more than one container click packet per set of actions*.
// *(exception being Java's "quick craft"/painting feature)
// Bedrock might, and this would generally fall into one of two categories:
// - Bedrock is sending an item directly from one slot to another, without picking it up, that cannot
// be expressed with a shift click
// - Bedrock wants to pick up or place an arbitrary amount of items that cannot be expressed from
// one left/right click action.
// When Bedrock does one of these actions and sends multiple packets, a 1.17.1+ server will
// increment the state ID on each confirmation packet it sends back (I.E. set slot). Then when it
// reads our next packet, because we kept the same state ID but the server incremented it, it'll be
// desynced and send the entire inventory contents back at us.
// This hack therefore increments the state ID to what the server will presumably send back to us.
// (This won't be perfect, but should get us through most vanilla situations, and if this is wrong the
// server will just send a set content packet back at us)
// Java typically doesn't increment the state ID if you send a vanilla-accurate container click packet,
// but it will increment the state ID with a vanilla client in at least the crafting table
if (inventory.getContainerType() == ContainerType.CRAFTING && CraftingInventoryTranslator.isCraftingGrid(action.slot)) {
// 1.18.1 sends a second set slot update for any action in the crafting grid
// And an additional packet if something is removed (Mojmap: CraftingContainer#removeItem)
//TODO this code kind of really sucks; it's potentially possible to see what Bedrock sends us and send a PlaceRecipePacket
int stateIdIncrements;
GeyserItemStack clicked = getItem(action.slot);
if (action.click == Click.LEFT) {
if (!clicked.isEmpty() && !InventoryUtils.canStack(simulatedCursor, clicked)) {
// An item is removed from the crafting table; yes deletion
stateIdIncrements = 3;
stateIdIncrements = 2;
} else {
// We can stack and we add all the items to the crafting slot; no deletion
stateIdIncrements = 2;
stateIdIncrements = 1;
}
} else if (action.click == Click.RIGHT) {
if (simulatedCursor.isEmpty() && !clicked.isEmpty()) {
// Items are taken; yes deletion
stateIdIncrements = 3;
} else if ((!simulatedCursor.isEmpty() && clicked.isEmpty()) || InventoryUtils.canStack(simulatedCursor, clicked)) {
// Adding our cursor item to the slot; no deletion
stateIdIncrements = 2;
} else {
// ?? nothing I guess
stateIdIncrements = 2;
}
stateIdIncrements = 1;
} else if (action.click.actionType == ContainerActionType.MOVE_TO_HOTBAR_SLOT) {
stateIdIncrements = 1;
} else {
if (session.getGeyser().getConfig().isDebugMode()) {
session.getGeyser().getLogger().debug("Not sure how to handle state ID hack in crafting table: " + plan);
}
stateIdIncrements = 2;
stateIdIncrements = 1;
}
inventory.incrementStateId(stateIdIncrements);
} else if (action.click.action instanceof MoveToHotbarAction) {
// Two slot changes sent
inventory.incrementStateId(2);
} else {
inventory.incrementStateId(1);
}
return stateId;
}
//TODO
private void reduceCraftingGrid(boolean makeAll) {
if (gridSize == -1)
return;
@ -370,9 +375,12 @@ public class ClickPlan {
}
for (int i = 0; i < gridSize; i++) {
GeyserItemStack item = getItem(i + 1);
if (!item.isEmpty())
item.sub(crafted);
final int slot = i + 1;
GeyserItemStack item = getItem(slot);
if (!item.isEmpty()) {
// These changes should be broadcasted to the server
sub(slot, item, crafted);
}
}
}
@ -385,6 +393,10 @@ public class ClickPlan {
for (ClickAction action : plan) {
if (translator.getSlotType(action.slot) == SlotType.NORMAL && action.slot != Click.OUTSIDE_SLOT) {
affectedSlots.add(action.slot);
if (action.click.actionType == ContainerActionType.MOVE_TO_HOTBAR_SLOT) {
//TODO won't work if offhand is added
affectedSlots.add(inventory.getOffsetForHotbar(((MoveToHotbarAction) action.click.action).ordinal()));
}
}
}
return affectedSlots;

View file

@ -28,9 +28,9 @@ package org.geysermc.geyser.network;
import com.github.steveice10.mc.protocol.codec.MinecraftCodec;
import com.github.steveice10.mc.protocol.codec.PacketCodec;
import com.nukkitx.protocol.bedrock.BedrockPacketCodec;
import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
import com.nukkitx.protocol.bedrock.v471.Bedrock_v471;
import com.nukkitx.protocol.bedrock.v475.Bedrock_v475;
import com.nukkitx.protocol.bedrock.v486.Bedrock_v486;
import java.util.ArrayList;
import java.util.Arrays;
@ -45,7 +45,7 @@ public final class MinecraftProtocol {
* Default Bedrock codec that should act as a fallback. Should represent the latest available
* release of the game that Geyser supports.
*/
public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v475.V475_CODEC;
public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v486.V486_CODEC;
/**
* A list of all supported Bedrock versions that can join Geyser
*/
@ -58,8 +58,8 @@ public final class MinecraftProtocol {
private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
static {
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v465.V465_CODEC);
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v471.V471_CODEC);
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v475.V475_CODEC.toBuilder().minecraftVersion("1.18.0/1.18.1/1.18.2").build());
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
}

View file

@ -0,0 +1,122 @@
/*
* Copyright (c) 2019-2022 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.registry;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.geysermc.geyser.registry.loader.RegistryLoader;
import org.jetbrains.annotations.Nullable;
import java.util.function.Supplier;
/**
* A mapped registry with an integer as the key. This class is designed to minimize the need for boxing/unboxing keys.
*
* @param <V> the value
*/
public class IntMappedRegistry<V> extends AbstractMappedRegistry<Integer, V, Int2ObjectMap<V>> {
protected <I> IntMappedRegistry(I input, RegistryLoader<I, Int2ObjectMap<V>> registryLoader) {
super(input, registryLoader);
}
/**
* Returns the value registered by the given integer.
*
* @param i the integer
* @return the value registered by the given integer.
*/
public V get(int i) {
return this.mappings.get(i);
}
@Nullable
@Override
@Deprecated
public V get(Integer key) {
return super.get(key);
}
/**
* Returns the value registered by the given key or the default value
* specified if null.
*
* @param i the key
* @param defaultValue the default value
* @return the value registered by the given key or the default value
* specified if null.
*/
public V getOrDefault(int i, V defaultValue) {
return this.mappings.getOrDefault(i, defaultValue);
}
@Override
@Deprecated
public V getOrDefault(Integer key, V defaultValue) {
return super.getOrDefault(key, defaultValue);
}
/**
* Registers a new value into this registry with the given key.
*
* @param i the key
* @param value the value
* @return a new value into this registry with the given key.
*/
public V register(int i, V value) {
return this.mappings.put(i, value);
}
@Override
@Deprecated
public V register(Integer key, V value) {
return super.register(key, value);
}
/**
* Creates a new integer mapped registry with the given {@link RegistryLoader}. The
* input type is not specified here, meaning the loader return type is either
* predefined, or the registry is populated at a later point.
*
* @param registryLoader the registry loader
* @param <I> the input
* @param <V> the map value
* @return a new registry with the given RegistryLoader
*/
public static <I, V> IntMappedRegistry<V> create(RegistryLoader<I, Int2ObjectMap<V>> registryLoader) {
return new IntMappedRegistry<>(null, registryLoader);
}
/**
* Creates a new integer mapped registry with the given {@link RegistryLoader} and input.
*
* @param registryLoader the registry loader
* @param <I> the input
* @param <V> the map value
* @return a new registry with the given RegistryLoader supplier
*/
public static <I, V> IntMappedRegistry<V> create(I input, Supplier<RegistryLoader<I, Int2ObjectMap<V>>> registryLoader) {
return new IntMappedRegistry<>(input, registryLoader.get());
}
}

View file

@ -44,20 +44,20 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.kyori.adventure.key.Key;
import org.geysermc.geyser.api.extension.ExtensionLoader;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.registry.populator.PacketRegistryPopulator;
import org.geysermc.geyser.translator.collision.BlockCollision;
import org.geysermc.geyser.inventory.item.Enchantment.JavaEnchantment;
import org.geysermc.geyser.translator.sound.SoundTranslator;
import org.geysermc.geyser.translator.sound.SoundInteractionTranslator;
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
import org.geysermc.geyser.registry.loader.*;
import org.geysermc.geyser.registry.populator.ItemRegistryPopulator;
import org.geysermc.geyser.registry.populator.PacketRegistryPopulator;
import org.geysermc.geyser.registry.populator.RecipeRegistryPopulator;
import org.geysermc.geyser.registry.type.EnchantmentData;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.registry.type.ParticleMapping;
import org.geysermc.geyser.registry.type.SoundMapping;
import org.geysermc.geyser.translator.collision.BlockCollision;
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
import org.geysermc.geyser.translator.sound.SoundInteractionTranslator;
import org.geysermc.geyser.translator.sound.SoundTranslator;
import java.util.EnumMap;
import java.util.List;
@ -96,7 +96,7 @@ public final class Registries {
/**
* A mapped registry containing which holds block IDs to its {@link BlockCollision}.
*/
public static final SimpleMappedRegistry<Integer, BlockCollision> COLLISIONS = SimpleMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new);
public static final IntMappedRegistry<BlockCollision> COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new);
/**
* A versioned registry which holds a {@link RecipeType} to a corresponding list of {@link CraftingData}.
@ -154,7 +154,7 @@ public final class Registries {
* A mapped registry holding the available records, with the ID of the record being the key, and the {@link com.nukkitx.protocol.bedrock.data.SoundEvent}
* as the value.
*/
public static final SimpleMappedRegistry<Integer, com.nukkitx.protocol.bedrock.data.SoundEvent> RECORDS = SimpleMappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
public static final IntMappedRegistry<com.nukkitx.protocol.bedrock.data.SoundEvent> RECORDS = IntMappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
/**
* A mapped registry holding sound identifiers to their corresponding {@link SoundMapping}.

View file

@ -50,10 +50,10 @@ import java.util.regex.Pattern;
/**
* Loads collision data from the given resource path.
*/
public class CollisionRegistryLoader extends MultiResourceRegistryLoader<String, Map<Integer, BlockCollision>> {
public class CollisionRegistryLoader extends MultiResourceRegistryLoader<String, Int2ObjectMap<BlockCollision>> {
@Override
public Map<Integer, BlockCollision> load(Pair<String, String> input) {
public Int2ObjectMap<BlockCollision> load(Pair<String, String> input) {
Int2ObjectMap<BlockCollision> collisions = new Int2ObjectOpenHashMap<>();
Map<Class<?>, CollisionInfo> annotationMap = new IdentityHashMap<>();

View file

@ -28,8 +28,8 @@ package org.geysermc.geyser.registry.populator;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableMap;
import com.nukkitx.nbt.*;
import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
import com.nukkitx.protocol.bedrock.v471.Bedrock_v471;
import com.nukkitx.protocol.bedrock.v486.Bedrock_v486;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
@ -46,7 +46,10 @@ import org.geysermc.geyser.util.BlockUtils;
import java.io.DataInputStream;
import java.io.InputStream;
import java.util.*;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.zip.GZIPInputStream;
@ -59,8 +62,34 @@ public class BlockRegistryPopulator {
static {
ImmutableMap.Builder<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> stateMapperBuilder = ImmutableMap.<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>>builder()
.put(ObjectIntPair.of("1_17_30", Bedrock_v465.V465_CODEC.getProtocolVersion()), EMPTY_MAPPER)
.put(ObjectIntPair.of("1_17_40", Bedrock_v471.V471_CODEC.getProtocolVersion()), EMPTY_MAPPER);
.put(ObjectIntPair.of("1_17_40", Bedrock_v471.V471_CODEC.getProtocolVersion()), EMPTY_MAPPER)
.put(ObjectIntPair.of("1_18_10", Bedrock_v486.V486_CODEC.getProtocolVersion()), (bedrockIdentifier, statesBuilder) -> {
statesBuilder.remove("no_drop_bit"); // Used in skulls
if (bedrockIdentifier.equals("minecraft:glow_lichen")) {
// Moved around north, south, west
int bits = (int) statesBuilder.get("multi_face_direction_bits");
boolean north = (bits & (1 << 2)) != 0;
boolean south = (bits & (1 << 3)) != 0;
boolean west = (bits & (1 << 4)) != 0;
if (north) {
bits |= 1 << 4;
} else {
bits &= ~(1 << 4);
}
if (south) {
bits |= 1 << 2;
} else {
bits &= ~(1 << 2);
}
if (west) {
bits |= 1 << 3;
} else {
bits &= ~(1 << 3);
}
statesBuilder.put("multi_face_direction_bits", bits);
}
return null;
});
BLOCK_MAPPERS = stateMapperBuilder.build();
}

View file

@ -35,9 +35,9 @@ import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.StartGamePacket;
import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
import com.nukkitx.protocol.bedrock.v471.Bedrock_v471;
import com.nukkitx.protocol.bedrock.v475.Bedrock_v475;
import com.nukkitx.protocol.bedrock.v486.Bedrock_v486;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
@ -63,9 +63,9 @@ public class ItemRegistryPopulator {
static {
PALETTE_VERSIONS = new Object2ObjectOpenHashMap<>();
PALETTE_VERSIONS.put("1_17_30", new PaletteVersion(Bedrock_v465.V465_CODEC.getProtocolVersion(), Collections.emptyMap()));
PALETTE_VERSIONS.put("1_17_40", new PaletteVersion(Bedrock_v471.V471_CODEC.getProtocolVersion(), Collections.emptyMap()));
PALETTE_VERSIONS.put("1_18_0", new PaletteVersion(Bedrock_v475.V475_CODEC.getProtocolVersion(), Collections.emptyMap()));
PALETTE_VERSIONS.put("1_18_10", new PaletteVersion(Bedrock_v486.V486_CODEC.getProtocolVersion(), Collections.emptyMap()));
}
private record PaletteVersion(int protocolVersion, Map<String, String> additionalTranslatedItems) {

View file

@ -38,10 +38,14 @@ import com.github.steveice10.mc.protocol.data.ProtocolState;
import com.github.steveice10.mc.protocol.data.UnexpectedEncryptionException;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.entity.player.HandPreference;
import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
import com.github.steveice10.mc.protocol.data.game.setting.ChatVisibility;
import com.github.steveice10.mc.protocol.data.game.setting.SkinPart;
import com.github.steveice10.mc.protocol.data.game.statistic.CustomStatistic;
import com.github.steveice10.mc.protocol.data.game.statistic.Statistic;
import com.github.steveice10.mc.protocol.packet.handshake.serverbound.ClientIntentionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientInformationPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
import com.github.steveice10.mc.protocol.packet.login.serverbound.ServerboundCustomQueryPacket;
@ -245,7 +249,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private Vector2i lastChunkPosition = null;
private int renderDistance;
@Setter
private int clientRenderDistance = -1;
private int serverRenderDistance;
// Exposed for GeyserConnect usage
protected boolean sentSpawnPacket;
@ -355,6 +361,15 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private Int2ObjectMap<IntList> stonecutterRecipes;
/**
* Starting in 1.17, Java servers expect the <code>carriedItem</code> parameter of the serverbound click container
* packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot
* contents before any transaction is done. With the current ViaVersion structure, if we do not send what 1.16 expects
* and send multiple click container packets, then successive transactions will be rejected.
*/
@Setter
private boolean emulatePost1_16Logic = true;
/**
* The current attack speed of the player. Used for sending proper cooldown timings.
* Setting a default fixes cooldowns not showing up on a fresh world.
@ -790,6 +805,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
FloodgateSkinUploader skinUploader = geyser.getSkinUploader();
FloodgateCipher cipher = geyser.getCipher();
String bedrockAddress = upstream.getAddress().getAddress().getHostAddress();
// both BungeeCord and Velocity remove the IPv6 scope (if there is one) for Spigot
int ipv6ScopeIndex = bedrockAddress.indexOf('%');
if (ipv6ScopeIndex != -1) {
bedrockAddress = bedrockAddress.substring(0, ipv6ScopeIndex);
}
encryptedData = cipher.encryptFromString(BedrockData.of(
clientData.getGameVersion(),
authData.name(),
@ -798,7 +820,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
clientData.getLanguageCode(),
clientData.getUiProfile().ordinal(),
clientData.getCurrentInputMode().ordinal(),
upstream.getAddress().getAddress().getHostAddress(),
bedrockAddress,
skinUploader.getId(),
skinUploader.getVerifyCode()
).toString());
@ -1160,9 +1182,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
return clientData.getLanguageCode();
}
public void setRenderDistance(int renderDistance) {
public void setServerRenderDistance(int renderDistance) {
renderDistance = GenericMath.ceil(++renderDistance * MathUtils.SQRT_OF_TWO); //square to circle
this.renderDistance = renderDistance;
this.serverRenderDistance = renderDistance;
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
chunkRadiusUpdatedPacket.setRadius(renderDistance);
@ -1420,6 +1442,27 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
sendUpstreamPacket(adventureSettingsPacket);
}
private int getRenderDistance() {
if (clientRenderDistance != -1) {
// The client has sent a render distance
return clientRenderDistance;
}
return serverRenderDistance;
}
// We need to send our skin parts to the server otherwise java sees us with no hat, jacket etc
private static final List<SkinPart> SKIN_PARTS = Arrays.asList(SkinPart.values());
/**
* Send a packet to the server to indicate client render distance, locale, skin parts, and hand preference.
*/
public void sendJavaClientSettings() {
ServerboundClientInformationPacket clientSettingsPacket = new ServerboundClientInformationPacket(locale(),
getRenderDistance(), ChatVisibility.FULL, true, SKIN_PARTS,
HandPreference.RIGHT_HAND, false, true);
sendDownstreamPacket(clientSettingsPacket);
}
/**
* Used for updating statistic values since we only get changes from the server
*

View file

@ -30,9 +30,6 @@ import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.geysermc.geyser.inventory.GeyserItemStack;
import javax.annotation.Nullable;
@ -43,7 +40,7 @@ import java.util.WeakHashMap;
* A temporary cache for lodestone information.
* Bedrock requests the lodestone position information separately from the item.
*/
public class LodestoneCache {
public final class LodestoneCache {
/**
* A list of any GeyserItemStacks that are lodestones. Used mainly to minimize Bedrock's "pop-in" effect
* when a new item has been created; instead we can re-use already existing IDs
@ -121,8 +118,16 @@ public class LodestoneCache {
}
public @Nullable LodestonePos getPos(int id) {
// We should not need to check the activeLodestones map as Bedrock should already be aware of this ID
return this.lodestones.remove(id);
LodestonePos pos = this.lodestones.remove(id);
if (pos != null) {
return pos;
}
for (LodestonePos activePos : this.activeLodestones.values()) {
if (activePos.id == id) {
return activePos;
}
}
return null;
}
public void clear() {
@ -131,16 +136,7 @@ public class LodestoneCache {
this.lodestones.clear();
}
@Getter
@AllArgsConstructor
@EqualsAndHashCode
public static class LodestonePos {
private final int id;
private final int x;
private final int y;
private final int z;
private final String dimension;
public record LodestonePos(int id, int x, int y, int z, String dimension) {
boolean equals(int x, int y, int z, String dimension) {
return this.x == x && this.y == y && this.z == z && this.dimension.equals(dimension);
}

View file

@ -115,10 +115,12 @@ public class SkinProvider {
WEARING_CUSTOM_SKULL = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkull\"}}", wearingCustomSkull, false);
String wearingCustomSkullSlim = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json"), StandardCharsets.UTF_8);
WEARING_CUSTOM_SKULL_SLIM = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkullSlim\"}}", wearingCustomSkullSlim, false);
}
public static void registerCacheImageTask(GeyserImpl geyser) {
// Schedule Daily Image Expiry if we are caching them
if (GeyserImpl.getInstance().getConfig().getCacheImages() > 0) {
GeyserImpl.getInstance().getScheduledThread().scheduleAtFixedRate(() -> {
if (geyser.getConfig().getCacheImages() > 0) {
geyser.getScheduledThread().scheduleAtFixedRate(() -> {
File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").toFile();
if (!cacheFolder.exists()) {
return;

View file

@ -38,17 +38,16 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequ
import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType;
import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
import it.unimi.dsi.fastutil.ints.IntSets;
import org.geysermc.geyser.inventory.BeaconContainer;
import org.geysermc.geyser.inventory.BedrockContainerSlot;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.inventory.BedrockContainerSlot;
import org.geysermc.geyser.inventory.holder.BlockInventoryHolder;
import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.InventoryUtils;
import java.util.Collections;
public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator {
public BeaconInventoryTranslator() {
super(1, new BlockInventoryHolder("minecraft:beacon", com.nukkitx.protocol.bedrock.data.inventory.ContainerType.BEACON) {
@ -104,7 +103,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator
}
@Override
public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
return action.getType() == StackRequestActionType.BEACON_PAYMENT;
}
@ -114,7 +113,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator
BeaconPaymentStackRequestActionData beaconPayment = (BeaconPaymentStackRequestActionData) request.getActions()[0];
ServerboundSetBeaconPacket packet = new ServerboundSetBeaconPacket(beaconPayment.getPrimaryEffect(), beaconPayment.getSecondaryEffect());
session.sendDownstreamPacket(packet);
return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet()));
return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
}
@Override

View file

@ -37,6 +37,11 @@ public class CraftingInventoryTranslator extends AbstractBlockInventoryTranslato
super(10, "minecraft:crafting_table", ContainerType.WORKBENCH, UIInventoryUpdater.INSTANCE);
}
@Override
public int getGridSize() {
return 9;
}
@Override
public SlotType getSlotType(int javaSlot) {
if (javaSlot == 0) {

View file

@ -27,23 +27,22 @@ package org.geysermc.geyser.translator.inventory;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerButtonClickPacket;
import com.nukkitx.protocol.bedrock.data.inventory.*;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
import com.nukkitx.protocol.bedrock.data.inventory.EnchantOptionData;
import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest;
import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.CraftRecipeStackRequestActionData;
import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData;
import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType;
import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
import com.nukkitx.protocol.bedrock.packet.PlayerEnchantOptionsPacket;
import org.geysermc.geyser.inventory.EnchantingContainer;
import org.geysermc.geyser.inventory.GeyserEnchantOption;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.inventory.BedrockContainerSlot;
import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
import it.unimi.dsi.fastutil.ints.IntSets;
import org.geysermc.geyser.inventory.*;
import org.geysermc.geyser.inventory.item.Enchantment;
import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
import org.geysermc.geyser.session.GeyserSession;
import java.util.Arrays;
import java.util.Collections;
public class EnchantingInventoryTranslator extends AbstractBlockInventoryTranslator {
public EnchantingInventoryTranslator() {
@ -104,7 +103,7 @@ public class EnchantingInventoryTranslator extends AbstractBlockInventoryTransla
}
@Override
public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
return action.getType() == StackRequestActionType.CRAFT_RECIPE;
}
@ -130,7 +129,7 @@ public class EnchantingInventoryTranslator extends AbstractBlockInventoryTransla
}
ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getId(), javaSlot);
session.sendDownstreamPacket(packet);
return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet()));
return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
}
@Override

View file

@ -26,12 +26,11 @@
package org.geysermc.geyser.translator.inventory;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient;
import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
@ -43,15 +42,10 @@ import it.unimi.dsi.fastutil.ints.*;
import lombok.AllArgsConstructor;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.CartographyContainer;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.inventory.BedrockContainerSlot;
import org.geysermc.geyser.inventory.SlotType;
import org.geysermc.geyser.inventory.*;
import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.inventory.click.ClickPlan;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTranslator;
import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator;
import org.geysermc.geyser.translator.inventory.furnace.BlastFurnaceInventoryTranslator;
@ -119,6 +113,13 @@ public abstract class InventoryTranslator {
public abstract SlotType getSlotType(int javaSlot);
public abstract Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory);
/**
* Used for crafting-related transactions. Will override in PlayerInventoryTranslator and CraftingInventoryTranslator.
*/
public int getGridSize() {
return -1;
}
/**
* Should be overwritten in cases where specific inventories should reject an item being in a specific spot.
* For examples, looms use this to reject items that are dyes in Bedrock but not in Java.
@ -136,7 +137,7 @@ public abstract class InventoryTranslator {
* Should be overrided if this request matches a certain criteria and shouldn't be treated normally.
* E.G. anvil renaming or enchanting
*/
public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
return false;
}
@ -147,7 +148,7 @@ public abstract class InventoryTranslator {
return rejectRequest(request);
}
public void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) {
public final void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) {
boolean refresh = false;
ItemStackResponsePacket responsePacket = new ItemStackResponsePacket();
for (ItemStackRequest request : requests) {
@ -199,10 +200,6 @@ public abstract class InventoryTranslator {
case PLACE: {
TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action;
if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) {
if (session.getGameMode().equals(GameMode.CREATIVE) && transferAction.getSource().getContainer() == ContainerSlotType.CRAFTING_INPUT &&
transferAction.getSource().getSlot() >= 28 && transferAction.getSource().getSlot() <= 31) {
return rejectRequest(request, false);
}
if (session.getGeyser().getConfig().isDebugMode()) {
session.getGeyser().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.name());
dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination());
@ -212,17 +209,19 @@ public abstract class InventoryTranslator {
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
int destSlot = bedrockSlotToJava(transferAction.getDestination());
boolean isSourceCursor = isCursor(transferAction.getSource());
boolean isDestCursor = isCursor(transferAction.getDestination());
if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(),
isCursor(transferAction.getSource()) ? -1 : sourceSlot,
transferAction.getDestination().getContainer(), isCursor(transferAction.getDestination()) ? -1 : destSlot)) {
isSourceCursor ? -1 : sourceSlot,
transferAction.getDestination().getContainer(), isDestCursor ? -1 : destSlot)) {
// This item would not be here in Java
return rejectRequest(request, false);
}
if (isCursor(transferAction.getSource()) && isCursor(transferAction.getDestination())) { //???
if (isSourceCursor && isDestCursor) { //???
return rejectRequest(request);
} else if (isCursor(transferAction.getSource())) { //releasing cursor
} else if (isSourceCursor) { //releasing cursor
int sourceAmount = cursor.getAmount();
if (transferAction.getCount() == sourceAmount) { //release all
plan.add(Click.LEFT, destSlot);
@ -231,7 +230,7 @@ public abstract class InventoryTranslator {
plan.add(Click.RIGHT, destSlot);
}
}
} else if (isCursor(transferAction.getDestination())) { //picking up into cursor
} else if (isDestCursor) { //picking up into cursor
GeyserItemStack sourceItem = plan.getItem(sourceSlot);
int sourceAmount = sourceItem.getAmount();
if (cursor.isEmpty()) { //picking up into empty cursor
@ -313,18 +312,7 @@ public abstract class InventoryTranslator {
if (!isSourceCursor && destination.getContainer() == ContainerSlotType.HOTBAR || destination.getContainer() == ContainerSlotType.HOTBAR_AND_INVENTORY) {
// Tell the server we're pressing one of the hotbar keys to save clicks
Click click = switch (destination.getSlot()) {
case 0 -> Click.SWAP_TO_HOTBAR_1;
case 1 -> Click.SWAP_TO_HOTBAR_2;
case 2 -> Click.SWAP_TO_HOTBAR_3;
case 3 -> Click.SWAP_TO_HOTBAR_4;
case 4 -> Click.SWAP_TO_HOTBAR_5;
case 5 -> Click.SWAP_TO_HOTBAR_6;
case 6 -> Click.SWAP_TO_HOTBAR_7;
case 7 -> Click.SWAP_TO_HOTBAR_8;
case 8 -> Click.SWAP_TO_HOTBAR_9;
default -> null;
};
Click click = InventoryUtils.getClickForHotbarSwap(destination.getSlot());
if (click != null) {
plan.add(click, sourceSlot);
break;
@ -442,6 +430,8 @@ public abstract class InventoryTranslator {
int leftover = 0;
ClickPlan plan = new ClickPlan(session, this, inventory);
// Track all the crafting table slots to report back the contents of the slots after crafting
IntSet affectedSlots = new IntOpenHashSet();
for (StackRequestActionData action : request.getActions()) {
switch (action.getType()) {
case CRAFT_RECIPE: {
@ -473,6 +463,7 @@ public abstract class InventoryTranslator {
return rejectRequest(request);
}
craftState = CraftState.INGREDIENTS;
affectedSlots.add(bedrockSlotToJava(((ConsumeStackRequestActionData) action).getSource()));
break;
}
case TAKE:
@ -533,21 +524,16 @@ public abstract class InventoryTranslator {
}
}
plan.execute(false);
return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots()));
affectedSlots.addAll(plan.getAffectedSlots());
return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
}
public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
int gridSize;
int gridDimensions;
if (this instanceof PlayerInventoryTranslator) {
gridSize = 4;
gridDimensions = 2;
} else if (this instanceof CraftingInventoryTranslator) {
gridSize = 9;
gridDimensions = 3;
} else {
final int gridSize = getGridSize();
if (gridSize == -1) {
return rejectRequest(request);
}
int gridDimensions = gridSize == 4 ? 2 : 3;
Recipe recipe;
Ingredient[] ingredients = new Ingredient[0];
@ -733,7 +719,7 @@ public abstract class InventoryTranslator {
/**
* Handled in {@link PlayerInventoryTranslator}
*/
public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
protected ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
return rejectRequest(request);
}
@ -768,14 +754,14 @@ public abstract class InventoryTranslator {
}
}
public static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List<ItemStackResponsePacket.ContainerEntry> containerEntries) {
protected static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List<ItemStackResponsePacket.ContainerEntry> containerEntries) {
return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.OK, request.getRequestId(), containerEntries);
}
/**
* Reject an incorrect ItemStackRequest.
*/
public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) {
protected static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) {
return rejectRequest(request, true);
}
@ -785,7 +771,7 @@ public abstract class InventoryTranslator {
* @param throwError whether this request was truly erroneous (true), or known as an outcome and should not be treated
* as bad (false).
*/
public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) {
protected static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) {
if (throwError && GeyserImpl.getInstance().getConfig().isDebugMode()) {
new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace();
}
@ -860,12 +846,15 @@ public abstract class InventoryTranslator {
return -1;
}
public List<ItemStackResponsePacket.ContainerEntry> makeContainerEntries(GeyserSession session, Inventory inventory, Set<Integer> affectedSlots) {
protected final List<ItemStackResponsePacket.ContainerEntry> makeContainerEntries(GeyserSession session, Inventory inventory, IntSet affectedSlots) {
Map<ContainerSlotType, List<ItemStackResponsePacket.ItemEntry>> containerMap = new HashMap<>();
for (int slot : affectedSlots) {
// Manually call iterator to prevent Integer boxing
IntIterator it = affectedSlots.iterator();
while (it.hasNext()) {
int slot = it.nextInt();
BedrockContainerSlot bedrockSlot = javaSlotToBedrockContainer(slot);
List<ItemStackResponsePacket.ItemEntry> list = containerMap.computeIfAbsent(bedrockSlot.getContainer(), k -> new ArrayList<>());
list.add(makeItemEntry(session, bedrockSlot.getSlot(), inventory.getItem(slot)));
List<ItemStackResponsePacket.ItemEntry> list = containerMap.computeIfAbsent(bedrockSlot.container(), k -> new ArrayList<>());
list.add(makeItemEntry(session, bedrockSlot.slot(), inventory.getItem(slot)));
}
List<ItemStackResponsePacket.ContainerEntry> containerEntries = new ArrayList<>();
@ -879,7 +868,7 @@ public abstract class InventoryTranslator {
return containerEntries;
}
public static ItemStackResponsePacket.ItemEntry makeItemEntry(GeyserSession session, int bedrockSlot, GeyserItemStack itemStack) {
private static ItemStackResponsePacket.ItemEntry makeItemEntry(GeyserSession session, int bedrockSlot, GeyserItemStack itemStack) {
ItemStackResponsePacket.ItemEntry itemEntry;
if (!itemStack.isEmpty()) {
// As of 1.16.210: Bedrock needs confirmation on what the current item durability is.

View file

@ -117,7 +117,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
}
@Override
public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
// If the LOOM_MATERIAL slot is not empty, we are crafting a pattern that does not come from an item
// Remove the CRAFT_NON_IMPLEMENTED_DEPRECATED when 1.17.30 is dropped
return (action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED || action.getType() == StackRequestActionType.CRAFT_LOOM)

View file

@ -35,6 +35,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.*;
import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
import it.unimi.dsi.fastutil.ints.IntIterator;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.geysermc.geyser.inventory.*;
@ -55,6 +56,11 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
super(46);
}
@Override
public int getGridSize() {
return 4;
}
@Override
public void updateInventory(GeyserSession session, Inventory inventory) {
updateCraftingGrid(session, inventory);
@ -370,14 +376,17 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
}
}
}
for (int slot : affectedSlots) {
// Manually call iterator to prevent Integer boxing
IntIterator it = affectedSlots.iterator();
while (it.hasNext()) {
int slot = it.nextInt();
sendCreativeAction(session, inventory, slot);
}
return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
}
@Override
public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
protected ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
ItemStack javaCreativeItem = null;
IntSet affectedSlots = new IntOpenHashSet();
CraftState craftState = CraftState.START;
@ -478,7 +487,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
return rejectRequest(request);
}
}
for (int slot : affectedSlots) {
// Manually call iterator to prevent Integer boxing
IntIterator it = affectedSlots.iterator();
while (it.hasNext()) {
int slot = it.nextInt();
sendCreativeAction(session, inventory, slot);
}
return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));

View file

@ -52,7 +52,7 @@ public class StonecutterInventoryTranslator extends AbstractBlockInventoryTransl
}
@Override
public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
// First is pre-1.18. TODO remove after 1.17.40 support is dropped and refactor stonecutter support to use CraftRecipeStackRequestActionData's recipe ID
return action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED || action.getType() == StackRequestActionType.CRAFT_RECIPE;
}

View file

@ -155,7 +155,7 @@ public class BannerTranslator extends ItemTranslator {
}
@Override
public ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
if (itemStack.getNbt() == null) {
return super.translateToBedrock(itemStack, mapping, mappings);
}
@ -163,9 +163,7 @@ public class BannerTranslator extends ItemTranslator {
ItemData.Builder builder = super.translateToBedrock(itemStack, mapping, mappings);
CompoundTag blockEntityTag = itemStack.getNbt().get("BlockEntityTag");
if (blockEntityTag != null && blockEntityTag.contains("Patterns")) {
ListTag patterns = blockEntityTag.get("Patterns");
if (blockEntityTag != null && blockEntityTag.get("Patterns") instanceof ListTag patterns) {
NbtMapBuilder nbtBuilder = builder.build().getTag().toBuilder(); //TODO fix ugly hack
if (patterns.equals(OMINOUS_BANNER_PATTERN)) {
// Remove the current patterns and set the ominous banner type

View file

@ -26,7 +26,9 @@
package org.geysermc.geyser.translator.inventory.item;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.github.steveice10.opennbt.tag.builtin.ByteTag;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.geyser.network.MinecraftProtocol;
import org.geysermc.geyser.registry.Registries;
@ -51,19 +53,30 @@ public class CompassTranslator extends ItemTranslator {
}
@Override
public ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
if (itemStack.getNbt() == null) return super.translateToBedrock(itemStack, mapping, mappings);
Tag lodestoneTag = itemStack.getNbt().get("LodestoneTracked");
if (lodestoneTag instanceof ByteTag) {
// Get the fake lodestonecompass entry
mapping = mappings.getStoredItems().lodestoneCompass();
// NBT will be translated in nbt/LodestoneCompassTranslator
protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
if (isLodestoneCompass(itemStack.getNbt())) {
// NBT will be translated in nbt/LodestoneCompassTranslator if applicable
return super.translateToBedrock(itemStack, mappings.getStoredItems().lodestoneCompass(), mappings);
}
return super.translateToBedrock(itemStack, mapping, mappings);
}
@Override
protected ItemMapping getItemMapping(int javaId, CompoundTag nbt, ItemMappings mappings) {
if (isLodestoneCompass(nbt)) {
return mappings.getStoredItems().lodestoneCompass();
}
return super.getItemMapping(javaId, nbt, mappings);
}
private boolean isLodestoneCompass(CompoundTag nbt) {
if (nbt != null) {
Tag lodestoneTag = nbt.get("LodestoneTracked");
return lodestoneTag instanceof ByteTag;
}
return false;
}
@Override
public ItemStack translateToJava(ItemData itemData, ItemMapping mapping, ItemMappings mappings) {
if (mapping.getBedrockIdentifier().equals("minecraft:lodestone_compass")) {

View file

@ -37,6 +37,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
@ -157,18 +158,13 @@ public abstract class ItemTranslator {
nbt = translateDisplayProperties(session, nbt, bedrockItem);
if (session.isAdvancedTooltips()) {
nbt = addAdvancedTooltips(nbt, session.getItemMappings().getMapping(stack), session.locale());
nbt = addAdvancedTooltips(nbt, bedrockItem, session.locale());
}
ItemStack itemStack = new ItemStack(stack.getId(), stack.getAmount(), nbt);
ItemData.Builder builder;
ItemTranslator itemStackTranslator = ITEM_STACK_TRANSLATORS.get(bedrockItem.getJavaId());
if (itemStackTranslator != null) {
builder = itemStackTranslator.translateToBedrock(itemStack, bedrockItem, session.getItemMappings());
} else {
builder = DEFAULT_TRANSLATOR.translateToBedrock(itemStack, bedrockItem, session.getItemMappings());
}
ItemTranslator itemStackTranslator = ITEM_STACK_TRANSLATORS.getOrDefault(bedrockItem.getJavaId(), DEFAULT_TRANSLATOR);
ItemData.Builder builder = itemStackTranslator.translateToBedrock(itemStack, bedrockItem, session.getItemMappings());
if (bedrockItem.isBlock()) {
builder.blockRuntimeId(bedrockItem.getBedrockBlockId());
}
@ -263,6 +259,19 @@ public abstract class ItemTranslator {
return canModifyBedrock;
}
/**
* Given an item stack, determine the item mapping that should be applied to Bedrock players.
*/
@Nonnull
public static ItemMapping getBedrockItemMapping(GeyserSession session, @Nonnull GeyserItemStack itemStack) {
if (itemStack.isEmpty()) {
return ItemMapping.AIR;
}
int javaId = itemStack.getJavaId();
return ITEM_STACK_TRANSLATORS.getOrDefault(javaId, DEFAULT_TRANSLATOR)
.getItemMapping(javaId, itemStack.getNbt(), session.getItemMappings());
}
private static final ItemTranslator DEFAULT_TRANSLATOR = new ItemTranslator() {
@Override
public List<ItemMapping> getAppliedItems() {
@ -270,7 +279,7 @@ public abstract class ItemTranslator {
}
};
public ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
if (itemStack == null) {
// Return, essentially, air
return ItemData.builder();
@ -295,6 +304,10 @@ public abstract class ItemTranslator {
public abstract List<ItemMapping> getAppliedItems();
protected ItemMapping getItemMapping(int javaId, CompoundTag nbt, ItemMappings mappings) {
return mappings.getMapping(javaId);
}
public NbtMap translateNbtToBedrock(CompoundTag tag) {
NbtMapBuilder builder = NbtMap.builder();
if (tag.getValue() != null && !tag.getValue().isEmpty()) {
@ -469,9 +482,8 @@ public abstract class ItemTranslator {
public static CompoundTag translateDisplayProperties(GeyserSession session, CompoundTag tag, ItemMapping mapping, char translationColor) {
boolean hasCustomName = false;
if (tag != null) {
CompoundTag display = tag.get("display");
if (display != null && display.contains("Name")) {
String name = ((StringTag) display.get("Name")).getValue();
if (tag.get("display") instanceof CompoundTag display && display.get("Name") instanceof StringTag tagName) {
String name = tagName.getValue();
// Get the translated name and prefix it with a reset char
name = MessageTranslator.convertMessageLenient(name, session.locale());
@ -491,8 +503,10 @@ public abstract class ItemTranslator {
if (tag == null) {
tag = new CompoundTag("");
}
CompoundTag display = tag.get("display");
if (display == null) {
CompoundTag display;
if (tag.get("display") instanceof CompoundTag oldDisplay) {
display = oldDisplay;
} else {
display = new CompoundTag("display");
// Add to the new root tag
tag.put(display);

View file

@ -54,7 +54,7 @@ public class PotionTranslator extends ItemTranslator {
}
@Override
public ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
if (itemStack.getNbt() == null) return super.translateToBedrock(itemStack, mapping, mappings);
Tag potionTag = itemStack.getNbt().get("Potion");
if (potionTag instanceof StringTag) {

View file

@ -59,7 +59,7 @@ public class TippedArrowTranslator extends ItemTranslator {
}
@Override
public ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
if (!mapping.getJavaIdentifier().equals("minecraft:tipped_arrow") || itemStack.getNbt() == null) {
// We're only concerned about minecraft:arrow when translating Bedrock -> Java
return super.translateToBedrock(itemStack, mapping, mappings);

View file

@ -51,13 +51,11 @@ public class BasicItemTranslator extends NbtItemStackTranslator {
}
}
CompoundTag displayTag = itemTag.get("display");
if (displayTag == null) {
if (!(itemTag.get("display") instanceof CompoundTag displayTag)) {
return;
}
Tag loreTag = displayTag.get("Lore");
if (loreTag instanceof ListTag listTag) {
if (displayTag.get("Lore") instanceof ListTag listTag) {
List<Tag> lore = new ArrayList<>();
for (Tag tag : listTag.getValue()) {
if (!(tag instanceof StringTag)) continue;

View file

@ -41,12 +41,14 @@ public class BedrockBlockEntityDataTranslator extends PacketTranslator<BlockEnti
@Override
public void translate(GeyserSession session, BlockEntityDataPacket packet) {
NbtMap tag = packet.getData();
if (tag.getString("id").equals("Sign")) {
String id = tag.getString("id");
if (id.equals("Sign")) {
String text = tag.getString("Text");
// This is the reason why this all works - Bedrock sends packets every time you update the sign, Java only wants the final packet
// But Bedrock sends one final packet when you're done editing the sign, which should be equal to the last message since there's no edits
// So if the latest update does not match the last cached update then it's still being edited
if (!tag.getString("Text").equals(session.getLastSignMessage())) {
session.setLastSignMessage(tag.getString("Text"));
if (!text.equals(session.getLastSignMessage())) {
session.setLastSignMessage(text);
return;
}
// Otherwise the two messages are identical and we can get to work deconstructing
@ -59,7 +61,7 @@ public class BedrockBlockEntityDataTranslator extends PacketTranslator<BlockEnti
// If it goes over the maximum, we need to start a new line to match Java
int widthCount = 0;
// This converts the message into the array'd message Java wants
for (char character : tag.getString("Text").toCharArray()) {
for (char character : text.toCharArray()) {
widthCount += SignUtils.getCharacterWidth(character);
// If we get a return in Bedrock, or go over the character width max, that signals to use the next line.
if (character == '\n' || widthCount > SignUtils.JAVA_CHARACTER_WIDTH_MAX) {
@ -111,7 +113,7 @@ public class BedrockBlockEntityDataTranslator extends PacketTranslator<BlockEnti
// We set the sign text cached in the session to null to indicate there is no work-in-progress sign
session.setLastSignMessage(null);
} else if (tag.getString("id").equals("JigsawBlock")) {
} else if (id.equals("JigsawBlock")) {
// Client has just sent a jigsaw block update
Position pos = new Position(tag.getInt("x"), tag.getInt("y"), tag.getInt("z"));
String name = tag.getString("name");

View file

@ -25,12 +25,14 @@
package org.geysermc.geyser.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
@ -40,21 +42,27 @@ import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
import com.nukkitx.protocol.bedrock.data.inventory.*;
import com.nukkitx.protocol.bedrock.packet.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.sound.EntitySoundInteractionTranslator;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.sound.EntitySoundInteractionTranslator;
import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.geyser.util.InventoryUtils;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@ -109,6 +117,23 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
case ITEM_USE:
switch (packet.getActionType()) {
case 0 -> {
Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace());
if (session.getGeyser().getConfig().isDisableBedrockScaffolding()) {
float yaw = session.getPlayerEntity().getYaw();
boolean isGodBridging = switch (packet.getBlockFace()) {
case 2 -> yaw <= -135f || yaw > 135f;
case 3 -> yaw <= 45f && yaw > -45f;
case 4 -> yaw > 45f && yaw <= 135f;
case 5 -> yaw <= -45f && yaw > -135f;
default -> false;
};
if (isGodBridging) {
restoreCorrectBlock(session, blockPos, packet);
return;
}
}
// Check to make sure the client isn't spamming interaction
// Based on Nukkit 1.0, with changes to ensure holding down still works
boolean hasAlreadyClicked = System.currentTimeMillis() - session.getLastInteractionTime() < 110.0 &&
@ -138,7 +163,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
}
Vector3i blockPos = BlockUtils.getBlockPosition(packet.getBlockPosition(), packet.getBlockFace());
/*
Checks to ensure that the range will be accepted by the server.
"Not in range" doesn't refer to how far a vanilla client goes (that's a whole other mess),
@ -253,16 +277,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.setInteracting(true);
}
case 1 -> {
if (packet.getActions().size() == 1 && packet.getLegacySlots().size() > 0) {
InventoryActionData actionData = packet.getActions().get(0);
LegacySetItemSlotData slotData = packet.getLegacySlots().get(0);
if (slotData.getContainerId() == 6 && actionData.getToItem().getId() != 0) {
// The player is trying to swap out an armor piece that already has an item in it
// Java Edition does not allow this; let's revert it
session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory());
}
}
// Handled when sneaking
if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) {
break;
@ -282,6 +296,43 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND);
session.sendDownstreamPacket(useItemPacket);
List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
if (packet.getActions().size() == 1 && legacySlots.size() > 0) {
InventoryActionData actionData = packet.getActions().get(0);
LegacySetItemSlotData slotData = legacySlots.get(0);
if (slotData.getContainerId() == 6 && actionData.getToItem().getId() != 0) {
// The player is trying to swap out an armor piece that already has an item in it
if (session.getGeyser().getConfig().isAlwaysQuickChangeArmor()) {
// Java doesn't know when a player is in its own inventory and not, so we
// can abuse this feature to send a swap inventory packet
int bedrockHotbarSlot = packet.getHotbarSlot();
Click click = InventoryUtils.getClickForHotbarSwap(bedrockHotbarSlot);
if (click != null && slotData.getSlots().length != 0) {
Inventory playerInventory = session.getPlayerInventory();
// Bedrock sends us the index of the slot in the armor container; armor in Java
// Edition is offset by 5 in the player inventory
int armorSlot = slotData.getSlots()[0] + 5;
GeyserItemStack armorSlotItem = playerInventory.getItem(armorSlot);
GeyserItemStack hotbarItem = playerInventory.getItem(playerInventory.getOffsetForHotbar(bedrockHotbarSlot));
playerInventory.setItem(armorSlot, hotbarItem, session);
playerInventory.setItem(bedrockHotbarSlot, armorSlotItem, session);
Int2ObjectMap<ItemStack> changedSlots = new Int2ObjectOpenHashMap<>(2);
changedSlots.put(armorSlot, hotbarItem.getItemStack());
changedSlots.put(bedrockHotbarSlot, armorSlotItem.getItemStack());
ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket(
playerInventory.getId(), playerInventory.getStateId(), armorSlot,
click.actionType, click.action, null, changedSlots);
session.sendDownstreamPacket(clickPacket);
}
} else {
// Disallowed; let's revert
session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory());
}
}
}
}
case 2 -> {
int blockState = session.getGameMode() == GameMode.CREATIVE ?

View file

@ -58,14 +58,14 @@ public class BedrockPositionTrackingDBClientRequestTranslator extends PacketTran
// Build the NBT data for the update
NbtMapBuilder builder = NbtMap.builder();
builder.putInt("dim", DimensionUtils.javaToBedrock(pos.getDimension()));
builder.putInt("dim", DimensionUtils.javaToBedrock(pos.dimension()));
builder.putString("id", "0x" + String.format("%08X", packet.getTrackingId()));
builder.putByte("version", (byte) 1); // Not sure what this is for
builder.putByte("status", (byte) 0); // Not sure what this is for
// Build the position for the update
builder.putList("pos", NbtType.INT, pos.getX(), pos.getY(), pos.getZ());
builder.putList("pos", NbtType.INT, pos.x(), pos.y(), pos.z());
broadcastPacket.setTag(builder.build());
session.sendUpstreamPacket(broadcastPacket);

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2019-2022 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.nukkitx.protocol.bedrock.packet.RequestChunkRadiusPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
/**
* Sent when the client updates its desired render distance.
*/
@Translator(packet = RequestChunkRadiusPacket.class)
public class BedrockRequestChunkRadiusTranslator extends PacketTranslator<RequestChunkRadiusPacket> {
@Override
public void translate(GeyserSession session, RequestChunkRadiusPacket packet) {
session.setClientRenderDistance(packet.getRadius());
if (session.isLoggedIn()) {
session.sendJavaClientSettings();
}
}
}

View file

@ -27,10 +27,7 @@ package org.geysermc.geyser.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.data.game.ClientCommand;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
import com.nukkitx.protocol.bedrock.packet.RespawnPacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -41,28 +38,6 @@ public class BedrockRespawnTranslator extends PacketTranslator<RespawnPacket> {
@Override
public void translate(GeyserSession session, RespawnPacket packet) {
if (packet.getState() == RespawnPacket.State.CLIENT_READY) {
// Previously we only sent the respawn packet before the server finished loading
// The message included was 'Otherwise when immediate respawn is on the client never loads'
// But I assume the new if statement below fixes that problem
RespawnPacket respawnPacket = new RespawnPacket();
respawnPacket.setRuntimeEntityId(0);
respawnPacket.setPosition(Vector3f.ZERO);
respawnPacket.setState(RespawnPacket.State.SERVER_READY);
session.sendUpstreamPacket(respawnPacket);
if (session.isSpawned()) {
// Client might be stuck; resend spawn information
PlayerEntity entity = session.getPlayerEntity();
entity.updateBedrockMetadata(); // TODO test?
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
movePlayerPacket.setPosition(entity.getPosition());
movePlayerPacket.setRotation(entity.getBedrockRotation());
movePlayerPacket.setMode(MovePlayerPacket.Mode.RESPAWN);
session.sendUpstreamPacket(movePlayerPacket);
}
ServerboundClientCommandPacket javaRespawnPacket = new ServerboundClientCommandPacket(ClientCommand.RESPAWN);
session.sendDownstreamPacket(javaRespawnPacket);
}

View file

@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.bedrock;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatPacket;
import com.nukkitx.protocol.bedrock.packet.TextPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
@ -44,6 +45,18 @@ public class BedrockTextTranslator extends PacketTranslator<TextPacket> {
return;
}
if (message.indexOf(ChatColor.ESCAPE) != -1) {
// Filter out all escape characters - Java doesn't let you type these
StringBuilder builder = new StringBuilder();
for (int i = 0; i < message.length(); i++) {
char c = message.charAt(i);
if (c != ChatColor.ESCAPE) {
builder.append(c);
}
}
message = builder.toString();
}
if (MessageTranslator.isTooLong(message, session)) {
return;
}

View file

@ -25,31 +25,25 @@
package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.data.game.entity.player.HandPreference;
import com.github.steveice10.mc.protocol.data.game.setting.ChatVisibility;
import com.github.steveice10.mc.protocol.data.game.setting.SkinPart;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientInformationPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket;
import com.nukkitx.protocol.bedrock.data.GameRuleData;
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.geyser.session.auth.AuthType;
import com.nukkitx.protocol.bedrock.packet.AdventureSettingsPacket;
import com.nukkitx.protocol.bedrock.packet.GameRulesChangedPacket;
import com.nukkitx.protocol.bedrock.packet.SetPlayerGameTypePacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.auth.AuthType;
import org.geysermc.geyser.translator.level.BiomeTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.level.BiomeTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.geyser.util.PluginMessageUtils;
import java.util.Arrays;
import java.util.List;
@Translator(packet = ClientboundLoginPacket.class)
public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket> {
private static final List<SkinPart> SKIN_PART_VALUES = Arrays.asList(SkinPart.values());
@Override
public void translate(GeyserSession session, ClientboundLoginPacket packet) {
@ -99,13 +93,10 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
session.setReducedDebugInfo(packet.isReducedDebugInfo());
session.setRenderDistance(packet.getViewDistance());
session.setServerRenderDistance(packet.getViewDistance());
// We need to send our skin parts to the server otherwise java sees us with no hat, jacket etc
String locale = session.locale();
// TODO customize
ServerboundClientInformationPacket infoPacket = new ServerboundClientInformationPacket(locale, (byte) session.getRenderDistance(), ChatVisibility.FULL, true, SKIN_PART_VALUES, HandPreference.RIGHT_HAND, false, true);
session.sendDownstreamPacket(infoPacket);
session.sendJavaClientSettings();
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));

View file

@ -31,7 +31,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import java.util.Arrays;
import java.util.Collections;
/**
* Used to list recipes that we can definitely use the recipe book for (and therefore save on packet usage)
@ -42,9 +42,11 @@ public class JavaRecipeTranslator extends PacketTranslator<ClientboundRecipePack
@Override
public void translate(GeyserSession session, ClientboundRecipePacket packet) {
if (packet.getAction() == UnlockRecipesAction.REMOVE) {
session.getUnlockedRecipes().removeAll(Arrays.asList(packet.getRecipes()));
for (String identifier : packet.getRecipes()) {
session.getUnlockedRecipes().remove(identifier);
}
} else {
session.getUnlockedRecipes().addAll(Arrays.asList(packet.getRecipes()));
Collections.addAll(session.getUnlockedRecipes(), packet.getRecipes());
}
}
}

View file

@ -46,6 +46,8 @@ public class JavaRespawnTranslator extends PacketTranslator<ClientboundRespawnPa
public void translate(GeyserSession session, ClientboundRespawnPacket packet) {
SessionPlayerEntity entity = session.getPlayerEntity();
session.setSpawned(false);
entity.setHealth(entity.getMaxHealth());
entity.getAttributes().put(GeyserAttributeType.HEALTH, entity.createHealthAttribute());

View file

@ -31,12 +31,14 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.SmithingRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.StoneCuttingRecipeData;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateRecipesPacket;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket;
import com.nukkitx.protocol.bedrock.v486.Bedrock_v486;
import it.unimi.dsi.fastutil.ints.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
@ -76,6 +78,8 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
// Get the last known network ID (first used for the pregenerated recipes) and increment from there.
int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1;
boolean applySmithingRecipes = session.getUpstream().getProtocolVersion() >= Bedrock_v486.V486_CODEC.getProtocolVersion();
Int2ObjectMap<Recipe> recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion()));
Int2ObjectMap<List<StoneCuttingRecipeData>> unsortedStonecutterData = new Int2ObjectOpenHashMap<>();
CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
@ -128,6 +132,27 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
data.add(stoneCuttingData);
// Save for processing after all recipes have been received
}
case SMITHING -> {
// Required to translate these as of 1.18.10, or else they cannot be crafted
if (!applySmithingRecipes) {
continue;
}
SmithingRecipeData recipeData = (SmithingRecipeData) recipe.getData();
ItemData output = ItemTranslator.translateToBedrock(session, recipeData.getResult());
for (ItemStack base : recipeData.getBase().getOptions()) {
ItemData bedrockBase = ItemTranslator.translateToBedrock(session, base);
for (ItemStack addition : recipeData.getAddition().getOptions()) {
ItemData bedrockAddition = ItemTranslator.translateToBedrock(session, addition);
UUID uuid = UUID.randomUUID();
craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(),
Arrays.asList(bedrockBase, bedrockAddition),
Collections.singletonList(output), uuid, "smithing_table", 2, netId++));
}
}
}
default -> {
List<CraftingData> craftingData = recipeTypes.get(recipe.getType());
if (craftingData != null) {

View file

@ -245,8 +245,8 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
float baseX = position.getX();
float baseY = position.getY();
float baseZ = position.getZ();
float height = entity.getDefinition().height();
float width = entity.getDefinition().width();
float height = entity.getBoundingBoxHeight();
float width = entity.getBoundingBoxWidth();
Random random = ThreadLocalRandom.current();
for (int i = 0; i < 20; i++) {
// Reconstruct the Java Edition (1.18.1) logic, but in floats

View file

@ -27,12 +27,12 @@ package org.geysermc.geyser.translator.protocol.java.entity;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityDataPacket;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.InteractiveTagManager;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.entity.InteractiveTagManager;
@Translator(packet = ClientboundSetEntityDataPacket.class)
public class JavaSetEntityDataTranslator extends PacketTranslator<ClientboundSetEntityDataPacket> {

View file

@ -86,13 +86,14 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
ChunkUtils.updateChunkPosition(session, pos.toInt());
session.getGeyser().getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.entity.player.spawn", packet.getX(), packet.getY(), packet.getZ()));
if (session.getGeyser().getConfig().isDebugMode()) {
session.getGeyser().getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.entity.player.spawn", packet.getX(), packet.getY(), packet.getZ()));
}
return;
}
Entity vehicle = session.getPlayerEntity().getVehicle();
if (packet.isDismountVehicle() && vehicle != null) {
Entity vehicle;
if (packet.isDismountVehicle() && (vehicle = session.getPlayerEntity().getVehicle()) != null) {
SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
linkPacket.setEntityLink(new EntityLinkData(vehicle.getGeyserId(), entity.getGeyserId(), EntityLinkData.Type.REMOVE, false, false));
session.sendUpstreamPacket(linkPacket);

View file

@ -26,6 +26,7 @@
package org.geysermc.geyser.translator.protocol.java.inventory;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetContentPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.session.GeyserSession;
@ -43,9 +44,26 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
if (inventory == null)
return;
inventory.setStateId(packet.getStateId());
int inventorySize = inventory.getSize();
for (int i = 0; i < packet.getItems().length; i++) {
if (i > inventorySize) {
GeyserImpl geyser = session.getGeyser();
geyser.getLogger().warning("ClientboundContainerSetContentPacket sent to " + session.name()
+ " that exceeds inventory size!");
if (geyser.getConfig().isDebugMode()) {
geyser.getLogger().debug(packet);
geyser.getLogger().debug(inventory);
}
InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) {
translator.updateInventory(session, inventory);
}
// 1.18.1 behavior: the previous items will be correctly set, but the state ID and carried item will not
// as this produces a stack trace on the client.
// If Java processes this correctly in the future, we can revert this behavior
return;
}
GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]);
inventory.setItem(i, newItem, session);
}
@ -55,6 +73,10 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
translator.updateInventory(session, inventory);
}
int stateId = packet.getStateId();
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
inventory.setStateId(stateId);
session.getPlayerInventory().setCursor(GeyserItemStack.from(packet.getCarriedItem()), session);
InventoryUtils.updateCursor(session);
}

View file

@ -30,7 +30,6 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient;
import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetSlotPacket;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
@ -40,17 +39,15 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.InventoryUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -72,14 +69,16 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
return;
// Intentional behavior here below the cursor; Minecraft 1.18.1 also does this.
inventory.setStateId(packet.getStateId());
int stateId = packet.getStateId();
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
inventory.setStateId(stateId);
InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) {
if (session.getCraftingGridFuture() != null) {
session.getCraftingGridFuture().cancel(false);
}
session.setCraftingGridFuture(session.scheduleInEventLoop(() -> updateCraftingGrid(session, packet, inventory, translator), 150, TimeUnit.MILLISECONDS));
updateCraftingGrid(session, packet.getSlot(), packet.getItem(), inventory, translator);
GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
@ -93,21 +92,23 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
}
}
private static void updateCraftingGrid(GeyserSession session, ClientboundContainerSetSlotPacket packet, Inventory inventory, InventoryTranslator translator) {
if (packet.getSlot() == 0) {
int gridSize;
if (translator instanceof PlayerInventoryTranslator) {
gridSize = 4;
} else if (translator instanceof CraftingInventoryTranslator) {
gridSize = 9;
} else {
return;
}
/**
* Checks for a changed output slot in the crafting grid, and ensures Bedrock sees the recipe.
*/
private static void updateCraftingGrid(GeyserSession session, int slot, ItemStack item, Inventory inventory, InventoryTranslator translator) {
if (slot != 0) {
return;
}
int gridSize = translator.getGridSize();
if (gridSize == -1) {
return;
}
if (packet.getItem() == null || packet.getItem().getId() == 0) {
return;
}
if (item == null || item.getId() == 0) {
return;
}
session.setCraftingGridFuture(session.scheduleInEventLoop(() -> {
int offset = gridSize == 4 ? 28 : 32;
int gridDimensions = gridSize == 4 ? 2 : 3;
int firstRow = -1, height = -1;
@ -135,62 +136,10 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
height += -firstRow + 1;
width += -firstCol + 1;
recipes:
for (Recipe recipe : session.getCraftingRecipes().values()) {
if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {
ShapedRecipeData data = (ShapedRecipeData) recipe.getData();
if (!data.getResult().equals(packet.getItem())) {
continue;
}
if (data.getWidth() != width || data.getHeight() != height || width * height != data.getIngredients().length) {
continue;
}
Ingredient[] ingredients = data.getIngredients();
if (!testShapedRecipe(ingredients, inventory, gridDimensions, firstRow, height, firstCol, width)) {
Ingredient[] mirroredIngredients = new Ingredient[data.getIngredients().length];
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
mirroredIngredients[col + (row * width)] = ingredients[(width - 1 - col) + (row * width)];
}
}
if (Arrays.equals(ingredients, mirroredIngredients) ||
!testShapedRecipe(mirroredIngredients, inventory, gridDimensions, firstRow, height, firstCol, width)) {
continue;
}
}
// Recipe is had, don't sent packet
return;
} else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) {
ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData();
if (!data.getResult().equals(packet.getItem())) {
continue;
}
for (int i = 0; i < data.getIngredients().length; i++) {
Ingredient ingredient = data.getIngredients()[i];
for (ItemStack itemStack : ingredient.getOptions()) {
boolean inventoryHasItem = false;
for (int j = 0; j < inventory.getSize(); j++) {
GeyserItemStack geyserItemStack = inventory.getItem(j);
if (geyserItemStack.isEmpty()) {
inventoryHasItem = itemStack == null || itemStack.getId() == 0;
if (inventoryHasItem) {
break;
}
} else if (itemStack.equals(geyserItemStack.getItemStack(1))) {
inventoryHasItem = true;
break;
}
}
if (!inventoryHasItem) {
continue recipes;
}
}
}
// Recipe is had, don't sent packet
return;
}
if (InventoryUtils.getValidRecipe(session, item, inventory::getItem, gridDimensions, firstRow,
height, firstCol, width) != null) {
// Recipe is already present on the client; don't send packet
return;
}
UUID uuid = UUID.randomUUID();
@ -216,7 +165,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
}
}
ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, packet.getItem());
ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, item);
// Cache this recipe so we know the client has received it
session.getCraftingRecipes().put(newRecipeId, new Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data));
@ -226,7 +175,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
width,
height,
Arrays.asList(ingredients),
Collections.singletonList(ItemTranslator.translateToBedrock(session, packet.getItem())),
Collections.singletonList(ItemTranslator.translateToBedrock(session, item)),
uuid,
"crafting_table",
0,
@ -246,33 +195,6 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
index++;
}
}
}
}
private static boolean testShapedRecipe(Ingredient[] ingredients, Inventory inventory, int gridDimensions, int firstRow, int height, int firstCol, int width) {
int ingredientIndex = 0;
for (int row = firstRow; row < height + firstRow; row++) {
for (int col = firstCol; col < width + firstCol; col++) {
GeyserItemStack geyserItemStack = inventory.getItem(col + (row * gridDimensions) + 1);
Ingredient ingredient = ingredients[ingredientIndex++];
if (ingredient.getOptions().length == 0) {
if (!geyserItemStack.isEmpty()) {
return false;
}
} else {
boolean inventoryHasItem = false;
for (ItemStack item : ingredient.getOptions()) {
if (Objects.equals(geyserItemStack.getItemStack(1), item)) {
inventoryHasItem = true;
break;
}
}
if (!inventoryHasItem) {
return false;
}
}
}
}
return true;
}, 150, TimeUnit.MILLISECONDS));
}
}

View file

@ -43,6 +43,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.MathUtils;
import java.util.ArrayList;
import java.util.List;
@ -99,16 +100,17 @@ public class JavaMerchantOffersTranslator extends PacketTranslator<ClientboundMe
recipe.putInt("maxUses", trade.isTradeDisabled() ? 0 : trade.getMaxUses());
recipe.putInt("traderExp", trade.getXp());
recipe.putFloat("priceMultiplierA", trade.getPriceMultiplier());
recipe.put("sell", getItemTag(session, trade.getOutput(), 0));
recipe.putFloat("priceMultiplierB", 0.0f);
recipe.putInt("buyCountB", trade.getSecondInput() != null ? trade.getSecondInput().getAmount() : 0);
recipe.putInt("buyCountA", trade.getFirstInput().getAmount());
recipe.putInt("demand", trade.getDemand());
recipe.put("sell", getItemTag(session, trade.getOutput()));
// The buy count before demand and special price adjustments
recipe.putInt("buyCountA", Math.max(trade.getFirstInput().getAmount(), 0));
recipe.putInt("buyCountB", trade.getSecondInput() != null ? Math.max(trade.getSecondInput().getAmount(), 0) : 0);
recipe.putInt("demand", trade.getDemand()); // Seems to have no effect
recipe.putInt("tier", packet.getVillagerLevel() > 0 ? packet.getVillagerLevel() - 1 : 0); // -1 crashes client
recipe.put("buyA", getItemTag(session, trade.getFirstInput(), trade.getSpecialPrice()));
if (trade.getSecondInput() != null) {
recipe.put("buyB", getItemTag(session, trade.getSecondInput(), 0));
}
recipe.put("buyA", getItemTag(session, trade.getFirstInput(), trade.getSpecialPrice(), trade.getDemand(), trade.getPriceMultiplier()));
recipe.put("buyB", getItemTag(session, trade.getSecondInput()));
recipe.putInt("uses", trade.getNumUses());
recipe.putByte("rewardExp", (byte) 1);
tags.add(recipe.build());
@ -144,12 +146,31 @@ public class JavaMerchantOffersTranslator extends PacketTranslator<ClientboundMe
session.sendUpstreamPacket(updateTradePacket);
}
private static NbtMap getItemTag(GeyserSession session, ItemStack stack, int specialPrice) {
ItemData itemData = ItemTranslator.translateToBedrock(session, stack);
private static NbtMap getItemTag(GeyserSession session, ItemStack stack) {
if (stack == null || stack.getAmount() <= 0) { // Negative item counts appear as air on Java
return NbtMap.EMPTY;
}
return getItemTag(session, stack, session.getItemMappings().getMapping(stack), stack.getAmount());
}
private static NbtMap getItemTag(GeyserSession session, ItemStack stack, int specialPrice, int demand, float priceMultiplier) {
if (stack == null || stack.getAmount() <= 0) { // Negative item counts appear as air on Java
return NbtMap.EMPTY;
}
ItemMapping mapping = session.getItemMappings().getMapping(stack);
// Bedrock expects all price adjustments to be applied to the item's count
int count = stack.getAmount() + ((int) Math.max(Math.floor(stack.getAmount() * demand * priceMultiplier), 0)) + specialPrice;
count = MathUtils.constrain(count, 1, mapping.getStackSize());
return getItemTag(session, stack, mapping, count);
}
private static NbtMap getItemTag(GeyserSession session, ItemStack stack, ItemMapping mapping, int count) {
ItemData itemData = ItemTranslator.translateToBedrock(session, stack);
NbtMapBuilder builder = NbtMap.builder();
builder.putByte("Count", (byte) (Math.max(itemData.getCount() + specialPrice, 1)));
builder.putByte("Count", (byte) count);
builder.putShort("Damage", (short) itemData.getDamage());
builder.putString("Name", mapping.getBedrockIdentifier());
if (itemData.getTag() != null) {

View file

@ -25,14 +25,7 @@
package org.geysermc.geyser.translator.protocol.java.level;
import com.github.steveice10.mc.protocol.data.game.level.event.BonemealGrowEventData;
import com.github.steveice10.mc.protocol.data.game.level.event.BreakBlockEventData;
import com.github.steveice10.mc.protocol.data.game.level.event.BreakPotionEventData;
import com.github.steveice10.mc.protocol.data.game.level.event.ComposterEventData;
import com.github.steveice10.mc.protocol.data.game.level.event.DragonFireballEventData;
import com.github.steveice10.mc.protocol.data.game.level.event.ParticleEvent;
import com.github.steveice10.mc.protocol.data.game.level.event.RecordEventData;
import com.github.steveice10.mc.protocol.data.game.level.event.SmokeEventData;
import com.github.steveice10.mc.protocol.data.game.level.event.*;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundLevelEventPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
@ -40,14 +33,13 @@ import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;
import com.nukkitx.protocol.bedrock.packet.TextPacket;
import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.text.MinecraftLocale;
import java.util.Collections;
import java.util.Locale;
@ -218,8 +210,7 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
case BREAK_EYE_OF_ENDER -> effectPacket.setType(LevelEventType.PARTICLE_EYE_OF_ENDER_DEATH);
case MOB_SPAWN -> effectPacket.setType(LevelEventType.PARTICLE_MOB_BLOCK_SPAWN); // TODO: Check, but I don't think I really verified this ever went into effect on Java
case BONEMEAL_GROW_WITH_SOUND, BONEMEAL_GROW -> {
effectPacket.setType((particleEvent == ParticleEvent.BONEMEAL_GROW
&& session.getUpstream().getProtocolVersion() >= Bedrock_v465.V465_CODEC.getProtocolVersion()) ? LevelEventType.PARTICLE_TURTLE_EGG : LevelEventType.PARTICLE_CROP_GROWTH);
effectPacket.setType(particleEvent == ParticleEvent.BONEMEAL_GROW ? LevelEventType.PARTICLE_TURTLE_EGG : LevelEventType.PARTICLE_CROP_GROWTH);
BonemealGrowEventData growEventData = (BonemealGrowEventData) packet.getData();
effectPacket.setData(growEventData.getParticleCount());

View file

@ -35,6 +35,6 @@ public class JavaSetChunkCacheRadiusTranslator extends PacketTranslator<Clientbo
@Override
public void translate(GeyserSession session, ClientboundSetChunkCacheRadiusPacket packet) {
session.setRenderDistance(packet.getViewDistance());
session.setServerRenderDistance(packet.getViewDistance());
}
}

View file

@ -121,7 +121,7 @@ public class ChunkUtils {
if (chunkPos == null || !chunkPos.equals(newChunkPos)) {
NetworkChunkPublisherUpdatePacket chunkPublisherUpdatePacket = new NetworkChunkPublisherUpdatePacket();
chunkPublisherUpdatePacket.setPosition(position);
chunkPublisherUpdatePacket.setRadius(session.getRenderDistance() << 4);
chunkPublisherUpdatePacket.setRadius(session.getServerRenderDistance() << 4);
session.sendUpstreamPacket(chunkPublisherUpdatePacket);
session.setLastChunkPosition(newChunkPos);

View file

@ -169,8 +169,8 @@ public class FileUtils {
* @return The byte array of the file
*/
public static byte[] readAllBytes(File file) {
try (InputStream inputStream = new FileInputStream(file)) {
return readAllBytes(inputStream);
try (InputStream stream = new FileInputStream(file)) {
return stream.readAllBytes();
} catch (IOException e) {
throw new RuntimeException("Cannot read " + file);
}
@ -182,30 +182,12 @@ public class FileUtils {
*/
public static byte[] readAllBytes(String resource) {
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(resource)) {
return readAllBytes(stream);
return stream.readAllBytes();
} catch (IOException e) {
throw new RuntimeException("Error while trying to read internal input stream!", e);
}
}
/**
* @param stream the InputStream to read off of
* @return the byte array of an InputStream
*/
public static byte[] readAllBytes(InputStream stream) {
try {
int size = stream.available();
byte[] bytes = new byte[size];
try (BufferedInputStream buf = new BufferedInputStream(stream)) {
//noinspection ResultOfMethodCallIgnored
buf.read(bytes, 0, bytes.length);
}
return bytes;
} catch (IOException e) {
throw new RuntimeException("Error while trying to read input stream!", e);
}
}
/**
* Read the lines of a file and return it as a stream
*

View file

@ -27,6 +27,11 @@ package org.geysermc.geyser.util;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient;
import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundPickItemPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
@ -41,6 +46,7 @@ import org.geysermc.geyser.inventory.Container;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
@ -50,6 +56,8 @@ import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTransl
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.ItemMapping;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@ -330,4 +338,131 @@ public class InventoryUtils {
session.sendUpstreamPacket(hotbarPacket);
// No need to send a Java packet as Bedrock sends a confirmation packet back that we translate
}
@Nullable
public static Click getClickForHotbarSwap(int slot) {
return switch (slot) {
case 0 -> Click.SWAP_TO_HOTBAR_1;
case 1 -> Click.SWAP_TO_HOTBAR_2;
case 2 -> Click.SWAP_TO_HOTBAR_3;
case 3 -> Click.SWAP_TO_HOTBAR_4;
case 4 -> Click.SWAP_TO_HOTBAR_5;
case 5 -> Click.SWAP_TO_HOTBAR_6;
case 6 -> Click.SWAP_TO_HOTBAR_7;
case 7 -> Click.SWAP_TO_HOTBAR_8;
case 8 -> Click.SWAP_TO_HOTBAR_9;
default -> null;
};
}
/**
* Test all known recipes to find a valid match
*
* @param output if not null, the recipe has to output this item
*/
@Nullable
public static Recipe getValidRecipe(final GeyserSession session, final @Nullable ItemStack output, final IntFunction<GeyserItemStack> inventoryGetter,
final int gridDimensions, final int firstRow, final int height, final int firstCol, final int width) {
int nonAirCount = 0; // Used for shapeless recipes for amount of items needed in recipe
for (int row = firstRow; row < height + firstRow; row++) {
for (int col = firstCol; col < width + firstCol; col++) {
if (!inventoryGetter.apply(col + (row * gridDimensions) + 1).isEmpty()) {
nonAirCount++;
}
}
}
recipes:
for (Recipe recipe : session.getCraftingRecipes().values()) {
if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {
ShapedRecipeData data = (ShapedRecipeData) recipe.getData();
if (output != null && !data.getResult().equals(output)) {
continue;
}
Ingredient[] ingredients = data.getIngredients();
if (data.getWidth() != width || data.getHeight() != height || width * height != ingredients.length) {
continue;
}
if (!testShapedRecipe(ingredients, inventoryGetter, gridDimensions, firstRow, height, firstCol, width)) {
Ingredient[] mirroredIngredients = new Ingredient[ingredients.length];
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
mirroredIngredients[col + (row * width)] = ingredients[(width - 1 - col) + (row * width)];
}
}
if (Arrays.equals(ingredients, mirroredIngredients) ||
!testShapedRecipe(mirroredIngredients, inventoryGetter, gridDimensions, firstRow, height, firstCol, width)) {
continue;
}
}
return recipe;
} else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) {
ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData();
if (output != null && !data.getResult().equals(output)) {
continue;
}
if (nonAirCount != data.getIngredients().length) {
// There is an amount of items on the crafting table that is not the same as the ingredient count so this is invalid
continue;
}
for (int i = 0; i < data.getIngredients().length; i++) {
Ingredient ingredient = data.getIngredients()[i];
for (ItemStack itemStack : ingredient.getOptions()) {
boolean inventoryHasItem = false;
// Iterate only over the crafting table to find this item
crafting:
for (int row = firstRow; row < height + firstRow; row++) {
for (int col = firstCol; col < width + firstCol; col++) {
GeyserItemStack geyserItemStack = inventoryGetter.apply(col + (row * gridDimensions) + 1);
if (geyserItemStack.isEmpty()) {
inventoryHasItem = itemStack == null || itemStack.getId() == 0;
if (inventoryHasItem) {
break crafting;
}
} else if (itemStack.equals(geyserItemStack.getItemStack(1))) {
inventoryHasItem = true;
break crafting;
}
}
}
if (!inventoryHasItem) {
continue recipes;
}
}
}
return recipe;
}
}
return null;
}
private static boolean testShapedRecipe(final Ingredient[] ingredients, final IntFunction<GeyserItemStack> inventoryGetter,
final int gridDimensions, final int firstRow, final int height, final int firstCol, final int width) {
int ingredientIndex = 0;
for (int row = firstRow; row < height + firstRow; row++) {
for (int col = firstCol; col < width + firstCol; col++) {
GeyserItemStack geyserItemStack = inventoryGetter.apply(col + (row * gridDimensions) + 1);
Ingredient ingredient = ingredients[ingredientIndex++];
if (ingredient.getOptions().length == 0) {
if (!geyserItemStack.isEmpty()) {
return false;
}
} else {
boolean inventoryHasItem = false;
for (ItemStack item : ingredient.getOptions()) {
if (Objects.equals(geyserItemStack.getItemStack(1), item)) {
inventoryHasItem = true;
break;
}
}
if (!inventoryHasItem) {
return false;
}
}
}
}
return true;
}
}

View file

@ -51,6 +51,10 @@
"name" : "minecraft:air",
"id" : -158
},
{
"name" : "minecraft:allay_spawn_egg",
"id" : 631
},
{
"name" : "minecraft:allow",
"id" : 210
@ -65,7 +69,7 @@
},
{
"name" : "minecraft:amethyst_shard",
"id" : 623
"id" : 625
},
{
"name" : "minecraft:ancient_debris",
@ -117,7 +121,7 @@
},
{
"name" : "minecraft:balloon",
"id" : 597
"id" : 598
},
{
"name" : "minecraft:bamboo",
@ -133,7 +137,7 @@
},
{
"name" : "minecraft:banner_pattern",
"id" : 627
"id" : 635
},
{
"name" : "minecraft:barrel",
@ -293,7 +297,7 @@
},
{
"name" : "minecraft:bleach",
"id" : 595
"id" : 596
},
{
"name" : "minecraft:blue_candle",
@ -317,7 +321,7 @@
},
{
"name" : "minecraft:boat",
"id" : 625
"id" : 633
},
{
"name" : "minecraft:bone",
@ -429,11 +433,11 @@
},
{
"name" : "minecraft:camera",
"id" : 592
"id" : 593
},
{
"name" : "minecraft:campfire",
"id" : 588
"id" : 589
},
{
"name" : "minecraft:candle",
@ -493,7 +497,7 @@
},
{
"name" : "minecraft:chain",
"id" : 617
"id" : 619
},
{
"name" : "minecraft:chain_command_block",
@ -575,6 +579,10 @@
"name" : "minecraft:clay_ball",
"id" : 384
},
{
"name" : "minecraft:client_request_placeholder_block",
"id" : -465
},
{
"name" : "minecraft:clock",
"id" : 393
@ -669,7 +677,7 @@
},
{
"name" : "minecraft:compound",
"id" : 593
"id" : 594
},
{
"name" : "minecraft:concrete",
@ -793,7 +801,7 @@
},
{
"name" : "minecraft:crimson_door",
"id" : 614
"id" : 616
},
{
"name" : "minecraft:crimson_double_slab",
@ -833,7 +841,7 @@
},
{
"name" : "minecraft:crimson_sign",
"id" : 612
"id" : 614
},
{
"name" : "minecraft:crimson_slab",
@ -1169,7 +1177,7 @@
},
{
"name" : "minecraft:dye",
"id" : 626
"id" : 634
},
{
"name" : "minecraft:egg",
@ -1697,7 +1705,7 @@
},
{
"name" : "minecraft:end_crystal",
"id" : 629
"id" : 637
},
{
"name" : "minecraft:end_gateway",
@ -1803,6 +1811,10 @@
"name" : "minecraft:fire_charge",
"id" : 509
},
{
"name" : "minecraft:firefly_spawn_egg",
"id" : 632
},
{
"name" : "minecraft:firework_rocket",
"id" : 519
@ -1855,6 +1867,14 @@
"name" : "minecraft:frame",
"id" : 513
},
{
"name" : "minecraft:frog_egg",
"id" : -468
},
{
"name" : "minecraft:frog_spawn_egg",
"id" : 628
},
{
"name" : "minecraft:frosted_ice",
"id" : 207
@ -1891,13 +1911,17 @@
"name" : "minecraft:glistering_melon_slice",
"id" : 434
},
{
"name" : "minecraft:globe_banner_pattern",
"id" : 588
},
{
"name" : "minecraft:glow_berries",
"id" : 630
"id" : 638
},
{
"name" : "minecraft:glow_frame",
"id" : 621
"id" : 623
},
{
"name" : "minecraft:glow_ink_sac",
@ -1913,7 +1937,7 @@
},
{
"name" : "minecraft:glow_stick",
"id" : 166
"id" : 601
},
{
"name" : "minecraft:glowingobsidian",
@ -1929,7 +1953,7 @@
},
{
"name" : "minecraft:goat_horn",
"id" : 622
"id" : 624
},
{
"name" : "minecraft:goat_spawn_egg",
@ -2109,11 +2133,11 @@
},
{
"name" : "minecraft:honey_bottle",
"id" : 591
"id" : 592
},
{
"name" : "minecraft:honeycomb",
"id" : 590
"id" : 591
},
{
"name" : "minecraft:honeycomb_block",
@ -2141,7 +2165,7 @@
},
{
"name" : "minecraft:ice_bomb",
"id" : 594
"id" : 595
},
{
"name" : "minecraft:infested_deepslate",
@ -2569,7 +2593,7 @@
},
{
"name" : "minecraft:lodestone_compass",
"id" : 600
"id" : 602
},
{
"name" : "minecraft:log",
@ -2613,7 +2637,7 @@
},
{
"name" : "minecraft:medicine",
"id" : 598
"id" : 599
},
{
"name" : "minecraft:medium_amethyst_bud",
@ -2723,9 +2747,13 @@
"name" : "minecraft:music_disc_mellohi",
"id" : 540
},
{
"name" : "minecraft:music_disc_otherside",
"id" : 627
},
{
"name" : "minecraft:music_disc_pigstep",
"id" : 618
"id" : 620
},
{
"name" : "minecraft:music_disc_stal",
@ -2751,6 +2779,14 @@
"name" : "minecraft:mycelium",
"id" : 110
},
{
"name" : "minecraft:mysterious_frame",
"id" : -466
},
{
"name" : "minecraft:mysterious_frame_slot",
"id" : -467
},
{
"name" : "minecraft:name_tag",
"id" : 548
@ -2777,7 +2813,7 @@
},
{
"name" : "minecraft:nether_sprouts",
"id" : 619
"id" : 621
},
{
"name" : "minecraft:nether_star",
@ -2797,7 +2833,7 @@
},
{
"name" : "minecraft:netherite_axe",
"id" : 605
"id" : 607
},
{
"name" : "minecraft:netherite_block",
@ -2805,43 +2841,43 @@
},
{
"name" : "minecraft:netherite_boots",
"id" : 610
"id" : 612
},
{
"name" : "minecraft:netherite_chestplate",
"id" : 608
"id" : 610
},
{
"name" : "minecraft:netherite_helmet",
"id" : 607
},
{
"name" : "minecraft:netherite_hoe",
"id" : 606
},
{
"name" : "minecraft:netherite_ingot",
"id" : 601
},
{
"name" : "minecraft:netherite_leggings",
"id" : 609
},
{
"name" : "minecraft:netherite_pickaxe",
"id" : 604
"name" : "minecraft:netherite_hoe",
"id" : 608
},
{
"name" : "minecraft:netherite_scrap",
"id" : 611
},
{
"name" : "minecraft:netherite_shovel",
"name" : "minecraft:netherite_ingot",
"id" : 603
},
{
"name" : "minecraft:netherite_leggings",
"id" : 611
},
{
"name" : "minecraft:netherite_pickaxe",
"id" : 606
},
{
"name" : "minecraft:netherite_scrap",
"id" : 613
},
{
"name" : "minecraft:netherite_shovel",
"id" : 605
},
{
"name" : "minecraft:netherite_sword",
"id" : 602
"id" : 604
},
{
"name" : "minecraft:netherrack",
@ -2887,6 +2923,10 @@
"name" : "minecraft:ocelot_spawn_egg",
"id" : 451
},
{
"name" : "minecraft:ochre_froglight",
"id" : -471
},
{
"name" : "minecraft:orange_candle",
"id" : -414
@ -2943,6 +2983,10 @@
"name" : "minecraft:parrot_spawn_egg",
"id" : 478
},
{
"name" : "minecraft:pearlescent_froglight",
"id" : -469
},
{
"name" : "minecraft:phantom_membrane",
"id" : 574
@ -3257,7 +3301,7 @@
},
{
"name" : "minecraft:rapid_fertilizer",
"id" : 596
"id" : 597
},
{
"name" : "minecraft:ravager_spawn_egg",
@ -3577,7 +3621,7 @@
},
{
"name" : "minecraft:soul_campfire",
"id" : 620
"id" : 622
},
{
"name" : "minecraft:soul_fire",
@ -3601,11 +3645,11 @@
},
{
"name" : "minecraft:sparkler",
"id" : 599
"id" : 600
},
{
"name" : "minecraft:spawn_egg",
"id" : 628
"id" : 636
},
{
"name" : "minecraft:spider_eye",
@ -3669,7 +3713,7 @@
},
{
"name" : "minecraft:spyglass",
"id" : 624
"id" : 626
},
{
"name" : "minecraft:squid_spawn_egg",
@ -3829,7 +3873,7 @@
},
{
"name" : "minecraft:suspicious_stew",
"id" : 589
"id" : 590
},
{
"name" : "minecraft:sweet_berries",
@ -3839,6 +3883,14 @@
"name" : "minecraft:sweet_berry_bush",
"id" : -207
},
{
"name" : "minecraft:tadpole_bucket",
"id" : 630
},
{
"name" : "minecraft:tadpole_spawn_egg",
"id" : 629
},
{
"name" : "minecraft:tallgrass",
"id" : 31
@ -3943,6 +3995,10 @@
"name" : "minecraft:unpowered_repeater",
"id" : 93
},
{
"name" : "minecraft:verdant_froglight",
"id" : -470
},
{
"name" : "minecraft:vex_spawn_egg",
"id" : 476
@ -3977,7 +4033,7 @@
},
{
"name" : "minecraft:warped_door",
"id" : 615
"id" : 617
},
{
"name" : "minecraft:warped_double_slab",
@ -3997,7 +4053,7 @@
},
{
"name" : "minecraft:warped_fungus_on_a_stick",
"id" : 616
"id" : 618
},
{
"name" : "minecraft:warped_hyphae",
@ -4021,7 +4077,7 @@
},
{
"name" : "minecraft:warped_sign",
"id" : 613
"id" : 615
},
{
"name" : "minecraft:warped_slab",

View file

@ -125,6 +125,13 @@ show-cooldown: title
# Controls if coordinates are shown to players.
show-coordinates: true
# Whether Bedrock players are blocked from performing their scaffolding-style bridging.
disable-bedrock-scaffolding: false
# Whether Bedrock players can right-click outside of their inventory to replace armor in their inventory, even if the
# armor slot is already occupied (which Java Edition doesn't allow)
always-quick-change-armor: false
# If set, when a Bedrock player performs any emote, it will swap the offhand and mainhand items, just like the Java Edition keybind
# There are three options this can be set to:
# disabled - the default/fallback, which doesn't apply this workaround

View file

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>2.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Geyser</name>
<description>Allows for players from Minecraft Bedrock Edition to join Minecraft Java Edition servers.</description>