Migrate to SERVER-AUTHORITATIVE MOVEMENT dun dun dunnnn

This commit is contained in:
Camotoy 2024-10-24 13:47:03 -04:00
parent 52ce17dee6
commit e17ad64d8c
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
13 changed files with 557 additions and 340 deletions

View file

@ -27,6 +27,7 @@ package org.geysermc.geyser.level.physics;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.util.TriState;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.GenericMath;
import org.cloudburstmc.math.vector.Vector3d;
@ -153,11 +154,10 @@ public class CollisionManager {
* the two versions. Will also send corrected movement packets back to Bedrock if they collide with pistons.
*
* @param bedrockPosition the current Bedrock position of the client
* @param onGround whether the Bedrock player is on the ground
* @param teleported whether the Bedrock player has teleported to a new position. If true, movement correction is skipped.
* @return the position to send to the Java server, or null to cancel sending the packet
*/
public @Nullable Vector3d adjustBedrockPosition(Vector3f bedrockPosition, boolean onGround, boolean teleported) {
public @Nullable CollisionResult adjustBedrockPosition(Vector3f bedrockPosition, boolean teleported) {
PistonCache pistonCache = session.getPistonCache();
// Bedrock clients tend to fall off of honey blocks, so we need to teleport them to the new position
if (pistonCache.isPlayerAttachedToHoney()) {
@ -176,7 +176,7 @@ public class CollisionManager {
playerBoundingBox.setMiddleY(position.getY() + playerBoundingBox.getSizeY() / 2);
playerBoundingBox.setMiddleZ(position.getZ());
return playerBoundingBox.getBottomCenter();
return new CollisionResult(playerBoundingBox.getBottomCenter(), TriState.NOT_SET);
}
Vector3d startingPos = playerBoundingBox.getBottomCenter();
@ -198,9 +198,9 @@ public class CollisionManager {
position = playerBoundingBox.getBottomCenter();
boolean newOnGround = adjustedMovement.getY() != movement.getY() && movement.getY() < 0 || onGround;
boolean newOnGround = adjustedMovement.getY() != movement.getY() && movement.getY() < 0;
// Send corrected position to Bedrock if they differ by too much to prevent de-syncs
if (onGround != newOnGround || movement.distanceSquared(adjustedMovement) > INCORRECT_MOVEMENT_THRESHOLD) {
if (/*onGround != newOnGround || */movement.distanceSquared(adjustedMovement) > INCORRECT_MOVEMENT_THRESHOLD) {
PlayerEntity playerEntity = session.getPlayerEntity();
// Client will dismount if on a vehicle
if (playerEntity.getVehicle() == null && pistonCache.getPlayerMotion().equals(Vector3f.ZERO) && !pistonCache.isPlayerSlimeCollision()) {
@ -208,12 +208,12 @@ public class CollisionManager {
}
}
if (!onGround) {
if (!newOnGround) {
// Trim the position to prevent rounding errors that make Java think we are clipping into a block
position = Vector3d.from(position.getX(), Double.parseDouble(DECIMAL_FORMAT.format(position.getY())), position.getZ());
}
return position;
return new CollisionResult(position, TriState.byBoolean(newOnGround));
}
// TODO: This makes the player look upwards for some reason, rotation values must be wrong

View file

@ -25,10 +25,11 @@
package org.geysermc.geyser.level.physics;
import net.kyori.adventure.util.TriState;
import org.cloudburstmc.math.vector.Vector3d;
/**
* Holds the result of a collision check.
*/
public record CollisionResult(Vector3d correctedMovement, boolean horizontalCollision) {
public record CollisionResult(Vector3d correctedMovement, TriState onGround) {
}

View file

@ -71,7 +71,6 @@ import org.cloudburstmc.protocol.bedrock.packet.MultiplayerSettingsPacket;
import org.cloudburstmc.protocol.bedrock.packet.NpcRequestPacket;
import org.cloudburstmc.protocol.bedrock.packet.PhotoInfoRequestPacket;
import org.cloudburstmc.protocol.bedrock.packet.PhotoTransferPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerHotbarPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
import org.cloudburstmc.protocol.bedrock.packet.PurchaseReceiptPacket;
@ -318,7 +317,7 @@ class CodecProcessor {
.updateSerializer(ClientCheatAbilityPacket.class, ILLEGAL_SERIALIZER)
.updateSerializer(CraftingEventPacket.class, ILLEGAL_SERIALIZER)
// Illegal unusued serverbound packets that relate to unused features
.updateSerializer(PlayerAuthInputPacket.class, ILLEGAL_SERIALIZER)
//.updateSerializer(PlayerAuthInputPacket.class, ILLEGAL_SERIALIZER) TODO keeping until we determine which packets should replace
.updateSerializer(ClientCacheBlobStatusPacket.class, ILLEGAL_SERIALIZER)
.updateSerializer(SubClientLoginPacket.class, ILLEGAL_SERIALIZER)
.updateSerializer(SubChunkRequestPacket.class, ILLEGAL_SERIALIZER)

View file

@ -290,7 +290,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
}
@Override
public PacketSignal handle(MovePlayerPacket packet) {
public PacketSignal handle(MovePlayerPacket packet) { // TODO
if (session.isLoggingIn()) {
SetTitlePacket titlePacket = new SetTitlePacket();
titlePacket.setType(SetTitlePacket.Type.ACTIONBAR);

View file

@ -161,6 +161,7 @@ import org.geysermc.geyser.session.cache.ChunkCache;
import org.geysermc.geyser.session.cache.EntityCache;
import org.geysermc.geyser.session.cache.EntityEffectCache;
import org.geysermc.geyser.session.cache.FormCache;
import org.geysermc.geyser.session.cache.InputCache;
import org.geysermc.geyser.session.cache.LodestoneCache;
import org.geysermc.geyser.session.cache.PistonCache;
import org.geysermc.geyser.session.cache.PreferencesCache;
@ -210,7 +211,6 @@ import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.Serverbound
import org.geysermc.mcprotocollib.protocol.packet.handshake.serverbound.ClientIntentionPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatCommandSignedPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket;
@ -276,6 +276,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private final EntityCache entityCache;
private final EntityEffectCache effectCache;
private final FormCache formCache;
private final InputCache inputCache;
private final LodestoneCache lodestoneCache;
private final PistonCache pistonCache;
private final PreferencesCache preferencesCache;
@ -523,12 +524,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private boolean placedBucket;
/**
* Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances.
*/
@Setter
private long lastMovementTimestamp = System.currentTimeMillis();
/**
* Used to send a ServerboundMoveVehiclePacket for every PlayerInputPacket after idling on a boat/horse for more than 100ms
*/
@ -672,6 +667,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.entityCache = new EntityCache(this);
this.effectCache = new EntityEffectCache();
this.formCache = new FormCache(this);
this.inputCache = new InputCache(this);
this.lodestoneCache = new LodestoneCache();
this.pistonCache = new PistonCache(this);
this.preferencesCache = new PreferencesCache(this);
@ -1266,18 +1262,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
protected void tick() {
try {
pistonCache.tick();
// Check to see if the player's position needs updating - a position update should be sent once every 3 seconds
if (spawned && (System.currentTimeMillis() - lastMovementTimestamp) > 3000) {
// Recalculate in case something else changed position
Vector3d position = collisionManager.adjustBedrockPosition(playerEntity.getPosition(), playerEntity.isOnGround(), false);
// A null return value cancels the packet
if (position != null) {
ServerboundMovePlayerPosPacket packet = new ServerboundMovePlayerPosPacket(playerEntity.isOnGround(), false, //FIXME
position.getX(), position.getY(), position.getZ());
sendDownstreamGamePacket(packet);
}
lastMovementTimestamp = System.currentTimeMillis();
}
if (worldBorder.isResizing()) {
worldBorder.resize();
@ -1668,7 +1652,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setChatRestrictionLevel(ChatRestrictionLevel.NONE);
startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT);
startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.SERVER);
startGamePacket.setRewindHistorySize(0);
startGamePacket.setServerAuthoritativeBlockBreaking(false);

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session.cache;
import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData;
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundPlayerInputPacket;
import java.util.Set;
public final class InputCache {
private final GeyserSession session;
private ServerboundPlayerInputPacket inputPacket = new ServerboundPlayerInputPacket(false, false, false, false, false, false, false);
private boolean lastHorizontalCollision;
private int ticksSinceLastMovePacket;
public InputCache(GeyserSession session) {
this.session = session;
}
public void processInputs(PlayerAuthInputPacket packet) {
// Input is sent to the server before packet positions, as of 1.21.2
Set<PlayerAuthInputData> bedrockInput = packet.getInputData();
var oldInputPacket = this.inputPacket;
// TODO when is UP_LEFT, etc. used?
this.inputPacket = this.inputPacket
.withForward(bedrockInput.contains(PlayerAuthInputData.UP))
.withBackward(bedrockInput.contains(PlayerAuthInputData.DOWN))
.withLeft(bedrockInput.contains(PlayerAuthInputData.LEFT))
.withRight(bedrockInput.contains(PlayerAuthInputData.RIGHT))
.withJump(bedrockInput.contains(PlayerAuthInputData.JUMPING)) // Looks like this only triggers when the JUMP key input is being pressed. There's also JUMP_DOWN?
.withShift(bedrockInput.contains(PlayerAuthInputData.SNEAKING))
.withSprint(bedrockInput.contains(PlayerAuthInputData.SPRINTING)); // SPRINTING will trigger even if the player isn't moving
if (oldInputPacket != this.inputPacket) { // Simple equality check is fine since we're checking for an instance change.
session.sendDownstreamGamePacket(this.inputPacket);
}
}
public void markPositionPacketSent() {
this.ticksSinceLastMovePacket = 0;
}
public boolean shouldSendPositionReminder() {
// NOTE: if we implement spectating entities, DO NOT TICK THIS LOGIC THEN.
return ++this.ticksSinceLastMovePacket >= 20;
}
public boolean lastHorizontalCollision() {
return lastHorizontalCollision;
}
public void setLastHorizontalCollision(boolean lastHorizontalCollision) {
this.lastHorizontalCollision = lastHorizontalCollision;
}
}

View file

@ -30,7 +30,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import org.cloudburstmc.math.vector.Vector3d;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
@ -42,7 +41,6 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.InventoryTra
import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.LegacySetItemSlotData;
import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
import org.geysermc.geyser.entity.EntityDefinitions;
@ -187,7 +185,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
default -> false;
};
if (isGodBridging) {
restoreCorrectBlock(session, blockPos, packet);
restoreCorrectBlock(session, blockPos);
return;
}
}
@ -207,7 +205,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, belowBlockPos);
BlockDefinition extendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock);
if (extendedCollisionDefinition != null && (System.currentTimeMillis() - session.getLastInteractionTime()) < 200) {
restoreCorrectBlock(session, blockPos, packet);
restoreCorrectBlock(session, blockPos);
return;
}
}
@ -227,7 +225,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
if (isIncorrectHeldItem(session, packet)) {
restoreCorrectBlock(session, blockPos, packet);
restoreCorrectBlock(session, blockPos);
return;
}
@ -247,7 +245,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
*/
// Blocks cannot be placed or destroyed outside of the world border
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
restoreCorrectBlock(session, blockPos, packet);
restoreCorrectBlock(session, blockPos);
return;
}
@ -256,7 +254,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
if (!canInteractWithBlock(session, playerPosition, packetBlockPosition)) {
restoreCorrectBlock(session, blockPos, packet);
restoreCorrectBlock(session, blockPos);
return;
}
@ -270,7 +268,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
double clickDistanceY = clickPositionFullY - blockCenter.getY();
double clickDistanceZ = clickPositionFullZ - blockCenter.getZ();
if (!(Math.abs(clickDistanceX) < 1.0000001D && Math.abs(clickDistanceY) < 1.0000001D && Math.abs(clickDistanceZ) < 1.0000001D)) {
restoreCorrectBlock(session, blockPos, packet);
restoreCorrectBlock(session, blockPos);
return;
}
@ -424,53 +422,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
}
}
case 2 -> {
int blockState = session.getGameMode() == GameMode.CREATIVE ?
session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()) : session.getBreakingBlock();
session.setLastBlockPlaced(null);
session.setLastBlockPlacePosition(null);
// Same deal with vanilla block placing as above.
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
restoreCorrectBlock(session, packet.getBlockPosition(), packet);
return;
}
Vector3f playerPosition = session.getPlayerEntity().getPosition();
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
if (!canInteractWithBlock(session, playerPosition, packet.getBlockPosition())) {
restoreCorrectBlock(session, packet.getBlockPosition(), packet);
return;
}
int sequence = session.getWorldCache().nextPredictionSequence();
session.getWorldCache().markPositionInSequence(packet.getBlockPosition());
// -1 means we don't know what block they're breaking
if (blockState == -1) {
blockState = Block.JAVA_AIR_ID;
}
LevelEventPacket blockBreakPacket = new LevelEventPacket();
blockBreakPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK);
blockBreakPacket.setPosition(packet.getBlockPosition().toFloat());
blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
session.sendUpstreamPacket(blockBreakPacket);
session.setBreakingBlock(-1);
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
if (itemFrameEntity != null) {
ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(),
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamGamePacket(attackPacket);
break;
}
PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], sequence);
session.sendDownstreamGamePacket(breakPacket);
}
}
break;
case ITEM_RELEASE:
@ -550,7 +501,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
}
private boolean canInteractWithBlock(GeyserSession session, Vector3f playerPosition, Vector3i packetBlockPosition) {
public static boolean canInteractWithBlock(GeyserSession session, Vector3f playerPosition, Vector3i packetBlockPosition) {
// ViaVersion sends this 1.20.5+ attribute also, so older servers will have correct range checks.
double blockInteractionRange = session.getPlayerEntity().getBlockInteractionRange();
@ -578,7 +529,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
* @param session the session of the Bedrock client
* @param blockPos the block position to restore
*/
private void restoreCorrectBlock(GeyserSession session, Vector3i blockPos, InventoryTransactionPacket packet) {
public static void restoreCorrectBlock(GeyserSession session, Vector3i blockPos) {
BlockState javaBlockState = session.getGeyser().getWorldManager().blockAt(session, blockPos);
BlockDefinition bedrockBlock = session.getBlockMappings().getBedrockBlock(javaBlockState);
@ -605,7 +556,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.sendUpstreamPacket(updateWaterPacket);
// Reset the item in hand to prevent "missing" blocks
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getHeldItemSlot()); // TODO test
}
private boolean isIncorrectHeldItem(GeyserSession session, InventoryTransactionPacket packet) {
@ -699,9 +650,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
float pitch = (float) -Math.toDegrees(Math.atan2(yDiff, xzHypot));
SessionPlayerEntity entity = session.getPlayerEntity();
ServerboundMovePlayerPosRotPacket returnPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), false, playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), entity.getYaw(), entity.getPitch());
ServerboundMovePlayerPosRotPacket returnPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), session.getInputCache().lastHorizontalCollision(),
playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), entity.getYaw(), entity.getPitch());
// This matches Java edition behavior
ServerboundMovePlayerPosRotPacket movementPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), false, playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), yaw, pitch);
ServerboundMovePlayerPosRotPacket movementPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), session.getInputCache().lastHorizontalCollision(),
playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), yaw, pitch);
session.sendDownstreamGamePacket(movementPacket);
if (session.getLookBackScheduledFuture() != null) {

View file

@ -29,14 +29,11 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.PlayerActionType;
import org.cloudburstmc.protocol.bedrock.data.PlayerBlockActionData;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.entity.type.Entity;
@ -44,7 +41,6 @@ import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.level.block.property.Properties;
import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.registry.BlockRegistries;
@ -52,8 +48,6 @@ import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.translator.item.CustomItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.geyser.util.CooldownUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
@ -61,107 +55,36 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractAction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
@Translator(packet = PlayerActionPacket.class)
public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket> {
import java.util.List;
@Override
public void translate(GeyserSession session, PlayerActionPacket packet) {
final class BedrockBlockActions {
static void translate(GeyserSession session, List<PlayerBlockActionData> playerActions) {
SessionPlayerEntity entity = session.getPlayerEntity();
// Send book update before any player action
if (packet.getAction() != PlayerActionType.RESPAWN) {
session.getBookEditCache().checkForSend();
session.getBookEditCache().checkForSend();
for (PlayerBlockActionData blockActionData : playerActions) {
handle(session, entity, blockActionData);
}
}
Vector3i vector = packet.getBlockPosition();
private static void handle(GeyserSession session, SessionPlayerEntity entity, PlayerBlockActionData blockActionData) {
PlayerActionType action = blockActionData.getAction();
Vector3i vector = blockActionData.getBlockPosition();
int blockFace = blockActionData.getFace();
switch (packet.getAction()) {
case RESPAWN -> {
// Respawn process is finished and the server and client are both OK with respawning.
EntityEventPacket eventPacket = new EntityEventPacket();
eventPacket.setRuntimeEntityId(entity.getGeyserId());
eventPacket.setType(EntityEventType.RESPAWN);
eventPacket.setData(0);
session.sendUpstreamPacket(eventPacket);
// Resend attributes or else in rare cases the user can think they're not dead when they are, upon joining the server
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
attributesPacket.setRuntimeEntityId(entity.getGeyserId());
attributesPacket.getAttributes().addAll(entity.getAttributes().values());
session.sendUpstreamPacket(attributesPacket);
// Bounding box must be sent after a player dies and respawns since 1.19.40
entity.updateBoundingBox();
// Needed here since 1.19.81 for dimension switching
session.getEntityCache().updateBossBars();
}
case START_SWIMMING -> {
if (!entity.getFlag(EntityFlag.SWIMMING)) {
ServerboundPlayerCommandPacket startSwimPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING);
session.sendDownstreamGamePacket(startSwimPacket);
session.setSwimming(true);
}
}
case STOP_SWIMMING -> {
// Prevent packet spam when Bedrock players are crawling near the edge of a block
if (!session.getCollisionManager().mustPlayerCrawlHere()) {
ServerboundPlayerCommandPacket stopSwimPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SPRINTING);
session.sendDownstreamGamePacket(stopSwimPacket);
session.setSwimming(false);
}
}
case START_GLIDE -> {
// Otherwise gliding will not work in creative
ServerboundPlayerAbilitiesPacket playerAbilitiesPacket = new ServerboundPlayerAbilitiesPacket(false);
session.sendDownstreamGamePacket(playerAbilitiesPacket);
sendPlayerGlideToggle(session, entity);
}
case STOP_GLIDE -> sendPlayerGlideToggle(session, entity);
case START_SNEAK -> {
ServerboundPlayerCommandPacket startSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
session.sendDownstreamGamePacket(startSneakPacket);
session.startSneaking();
}
case STOP_SNEAK -> {
ServerboundPlayerCommandPacket stopSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SNEAKING);
session.sendDownstreamGamePacket(stopSneakPacket);
session.stopSneaking();
}
case START_SPRINT -> {
if (!entity.getFlag(EntityFlag.SWIMMING)) {
ServerboundPlayerCommandPacket startSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING);
session.sendDownstreamGamePacket(startSprintPacket);
session.setSprinting(true);
}
}
case STOP_SPRINT -> {
if (!entity.getFlag(EntityFlag.SWIMMING)) {
ServerboundPlayerCommandPacket stopSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SPRINTING);
session.sendDownstreamGamePacket(stopSprintPacket);
}
session.setSprinting(false);
}
switch (action) {
case DROP_ITEM -> {
ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM,
vector, Direction.VALUES[packet.getFace()], 0);
vector, Direction.VALUES[blockFace], 0);
session.sendDownstreamGamePacket(dropItemPacket);
}
case STOP_SLEEP -> {
ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED);
session.sendDownstreamGamePacket(stopSleepingPacket);
}
case START_BREAK -> {
// Ignore START_BREAK when the player is CREATIVE to avoid Spigot receiving 2 packets it interpets as block breaking. https://github.com/GeyserMC/Geyser/issues/4021
if (session.getGameMode() == GameMode.CREATIVE) {
@ -191,9 +114,9 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
session.sendUpstreamPacket(startBreak);
// Account for fire - the client likes to hit the block behind.
Vector3i fireBlockPos = BlockUtils.getBlockPosition(vector, packet.getFace());
Vector3i fireBlockPos = BlockUtils.getBlockPosition(vector, blockFace);
Block block = session.getGeyser().getWorldManager().blockAt(session, fireBlockPos).block();
Direction direction = Direction.VALUES[packet.getFace()];
Direction direction = Direction.VALUES[blockFace];
if (block == Blocks.FIRE || block == Blocks.SOUL_FIRE) {
ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos,
direction, session.getWorldCache().nextPredictionSequence());
@ -218,7 +141,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
Vector3f vectorFloat = vector.toFloat();
BlockState breakingBlockState = BlockState.of(breakingBlock);
Direction direction = Direction.VALUES[packet.getFace()];
Direction direction = Direction.VALUES[blockFace];
spawnBlockBreakParticles(session, direction, vector, breakingBlockState);
double breakTime = BlockUtils.getSessionBreakTime(session, breakingBlockState.block()) * 20;
@ -304,69 +227,10 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
session.sendUpstreamPacket(animatePacket);
}
}
case START_FLYING -> { // Since 1.20.30
if (session.isCanFly()) {
if (session.getGameMode() == GameMode.SPECTATOR) {
// should already be flying
session.sendAdventureSettings();
break;
}
if (session.getPlayerEntity().getFlag(EntityFlag.SWIMMING) && session.getCollisionManager().isPlayerInWater()) {
// As of 1.18.1, Java Edition cannot fly while in water, but it can fly while crawling
// If this isn't present, swimming on a 1.13.2 server and then attempting to fly will put you into a flying/swimming state that is invalid on JE
session.sendAdventureSettings();
break;
}
session.setFlying(true);
session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(true));
} else {
// update whether we can fly
session.sendAdventureSettings();
// stop flying
PlayerActionPacket stopFlyingPacket = new PlayerActionPacket();
stopFlyingPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
stopFlyingPacket.setAction(PlayerActionType.STOP_FLYING);
stopFlyingPacket.setBlockPosition(Vector3i.ZERO);
stopFlyingPacket.setResultPosition(Vector3i.ZERO);
stopFlyingPacket.setFace(0);
session.sendUpstreamPacket(stopFlyingPacket);
}
}
case STOP_FLYING -> {
session.setFlying(false);
session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(false));
}
case DIMENSION_CHANGE_REQUEST_OR_CREATIVE_DESTROY_BLOCK -> { // Used by client to get book from lecterns and items from item frame in creative mode since 1.20.70
BlockState state = session.getGeyser().getWorldManager().blockAt(session, vector);
if (state.getValue(Properties.HAS_BOOK, false)) {
session.setDroppingLecternBook(true);
ServerboundUseItemOnPacket blockPacket = new ServerboundUseItemOnPacket(
vector,
Direction.DOWN,
Hand.MAIN_HAND,
0, 0, 0,
false,
false,
session.getWorldCache().nextPredictionSequence());
session.sendDownstreamGamePacket(blockPacket);
break;
}
Entity itemFrame = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
if (itemFrame != null) {
ServerboundInteractPacket interactPacket = new ServerboundInteractPacket(itemFrame.getEntityId(),
InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
session.sendDownstreamGamePacket(interactPacket);
}
}
}
}
private void spawnBlockBreakParticles(GeyserSession session, Direction direction, Vector3i position, BlockState blockState) {
private static void spawnBlockBreakParticles(GeyserSession session, Direction direction, Vector3i position, BlockState blockState) {
LevelEventPacket levelEventPacket = new LevelEventPacket();
switch (direction) {
case UP -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_UP);
@ -380,9 +244,4 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
levelEventPacket.setData(session.getBlockMappings().getBedrockBlock(blockState).getRuntimeId());
session.sendUpstreamPacket(levelEventPacket);
}
private void sendPlayerGlideToggle(GeyserSession session, Entity entity) {
ServerboundPlayerCommandPacket glidePacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_ELYTRA_FLYING);
session.sendDownstreamGamePacket(glidePacket);
}
}

View file

@ -27,34 +27,33 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import org.cloudburstmc.math.vector.Vector3d;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData;
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.level.physics.CollisionResult;
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.mcprotocollib.network.packet.Packet;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerRotPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerStatusOnlyPacket;
@Translator(packet = MovePlayerPacket.class)
public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPacket> {
@Override
public void translate(GeyserSession session, MovePlayerPacket packet) {
public final class BedrockMovePlayerTranslator {
static void translate(GeyserSession session, PlayerAuthInputPacket packet) {
SessionPlayerEntity entity = session.getPlayerEntity();
if (!session.isSpawned()) return;
session.setLastMovementTimestamp(System.currentTimeMillis());
// Send book update before the player moves
session.getBookEditCache().checkForSend();
boolean actualPositionChanged = !entity.getPosition().equals(packet.getPosition());
// Ignore movement packets until Bedrock's position matches the teleported position
if (session.getUnconfirmedTeleport() != null) {
if (session.getUnconfirmedTeleport() != null && actualPositionChanged) {
session.confirmTeleport(packet.getPosition().toDouble().sub(0, EntityDefinitions.PLAYER.offset(), 0));
return;
}
@ -70,7 +69,8 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
float pitch = packet.getRotation().getX();
float headYaw = packet.getRotation().getY();
boolean positionChanged = !entity.getPosition().equals(packet.getPosition());
// shouldSendPositionReminder also increments a tick counter, so make sure it's always called.
boolean positionChanged = session.getInputCache().shouldSendPositionReminder() || actualPositionChanged;
boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw;
if (session.getLookBackScheduledFuture() != null) {
@ -80,19 +80,22 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
session.setLookBackScheduledFuture(null);
}
// This takes into account no movement sent from the client, but the player is trying to move anyway.
// (Press into a wall in a corner - you're trying to move but nothing actually happens)
boolean horizontalCollision = packet.getInputData().contains(PlayerAuthInputData.HORIZONTAL_COLLISION);
// If only the pitch and yaw changed
// This isn't needed, but it makes the packets closer to vanilla
// It also means you can't "lag back" while only looking, in theory
if (!positionChanged && rotationChanged) {
ServerboundMovePlayerRotPacket playerRotationPacket = new ServerboundMovePlayerRotPacket(packet.isOnGround(), false, yaw, pitch);
ServerboundMovePlayerRotPacket playerRotationPacket = new ServerboundMovePlayerRotPacket(entity.isOnGround(), horizontalCollision, yaw, pitch);
entity.setYaw(yaw);
entity.setPitch(pitch);
entity.setHeadYaw(headYaw);
entity.setOnGround(packet.isOnGround());
session.sendDownstreamGamePacket(playerRotationPacket);
} else {
} else if (positionChanged) {
// World border collision will be handled by client vehicle
if (!(entity.getVehicle() instanceof ClientVehicle clientVehicle && clientVehicle.isClientControlled())
&& session.getWorldBorder().isPassingIntoBorderBoundaries(packet.getPosition(), true)) {
@ -100,9 +103,10 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
}
if (isValidMove(session, entity.getPosition(), packet.getPosition())) {
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround(), packet.getMode() == MovePlayerPacket.Mode.TELEPORT);
if (position != null) { // A null return value cancels the packet
boolean onGround = packet.isOnGround();
CollisionResult result = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.getInputData().contains(PlayerAuthInputData.HANDLE_TELEPORT));
if (result != null) { // A null return value cancels the packet
Vector3d position = result.correctedMovement();
boolean onGround = result.onGround().toBooleanOrElse(entity.isOnGround());
boolean isBelowVoid = entity.isVoidPositionDesynched();
boolean teleportThroughVoidFloor, mustResyncPosition;
@ -138,7 +142,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
// Send rotation updates as well
movePacket = new ServerboundMovePlayerPosRotPacket(
onGround,
false,
horizontalCollision,
position.getX(), yPosition, position.getZ(),
yaw, pitch
);
@ -147,7 +151,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
entity.setHeadYaw(headYaw);
} else {
// Rotation did not change; don't send an update with rotation
movePacket = new ServerboundMovePlayerPosPacket(onGround, false, position.getX(), yPosition, position.getZ());
movePacket = new ServerboundMovePlayerPosPacket(onGround, horizontalCollision, position.getX(), yPosition, position.getZ());
}
entity.setPositionManual(packet.getPosition());
@ -162,6 +166,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
entity.teleportVoidFloorFix(true);
}
session.getInputCache().markPositionPacketSent();
session.getSkullCache().updateVisibleSkulls();
}
} else {
@ -169,8 +174,12 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
session.getGeyser().getLogger().debug("Recalculating position...");
session.getCollisionManager().recalculatePosition();
}
} else if (horizontalCollision != session.getInputCache().lastHorizontalCollision()) {
session.sendDownstreamGamePacket(new ServerboundMovePlayerStatusOnlyPacket(entity.isOnGround(), horizontalCollision));
}
session.getInputCache().setLastHorizontalCollision(horizontalCollision);
// Move parrots to match if applicable
if (entity.getLeftParrot() != null) {
entity.getLeftParrot().moveAbsolute(entity.getPosition(), entity.getYaw(), entity.getPitch(), entity.getHeadYaw(), true, false);
@ -180,11 +189,11 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
}
}
private boolean isInvalidNumber(float val) {
private static boolean isInvalidNumber(float val) {
return Float.isNaN(val) || Float.isInfinite(val);
}
private boolean isValidMove(GeyserSession session, Vector3f currentPosition, Vector3f newPosition) {
private static boolean isValidMove(GeyserSession session, Vector3f currentPosition, Vector3f newPosition) {
if (isInvalidNumber(newPosition.getX()) || isInvalidNumber(newPosition.getY()) || isInvalidNumber(newPosition.getZ())) {
return false;
}

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.level.block.property.Properties;
import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractAction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
@Translator(packet = PlayerActionPacket.class)
public class BedrockPlayerActionTranslator extends PacketTranslator<PlayerActionPacket> {
@Override
public void translate(GeyserSession session, PlayerActionPacket packet) {
// This packet was used more before server auth movement was needed, but it's still used for a couple things...
switch (packet.getAction()) {
case RESPAWN -> {
SessionPlayerEntity entity = session.getPlayerEntity();
// Respawn process is finished and the server and client are both OK with respawning.
EntityEventPacket eventPacket = new EntityEventPacket();
eventPacket.setRuntimeEntityId(entity.getGeyserId());
eventPacket.setType(EntityEventType.RESPAWN);
eventPacket.setData(0);
session.sendUpstreamPacket(eventPacket);
// Resend attributes or else in rare cases the user can think they're not dead when they are, upon joining the server
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
attributesPacket.setRuntimeEntityId(entity.getGeyserId());
attributesPacket.getAttributes().addAll(entity.getAttributes().values());
session.sendUpstreamPacket(attributesPacket);
// Bounding box must be sent after a player dies and respawns since 1.19.40
entity.updateBoundingBox();
// Needed here since 1.19.81 for dimension switching
session.getEntityCache().updateBossBars();
}
case STOP_SLEEP -> {
ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(session.getPlayerEntity().getEntityId(), PlayerState.LEAVE_BED);
session.sendDownstreamGamePacket(stopSleepingPacket);
}
case DIMENSION_CHANGE_REQUEST_OR_CREATIVE_DESTROY_BLOCK -> { // Used by client to get book from lecterns and items from item frame in creative mode since 1.20.70
Vector3i vector = packet.getBlockPosition();
BlockState state = session.getGeyser().getWorldManager().blockAt(session, vector);
if (state.getValue(Properties.HAS_BOOK, false)) {
session.setDroppingLecternBook(true);
ServerboundUseItemOnPacket blockPacket = new ServerboundUseItemOnPacket(
vector,
Direction.DOWN,
Hand.MAIN_HAND,
0, 0, 0,
false,
false,
session.getWorldCache().nextPredictionSequence());
session.sendDownstreamGamePacket(blockPacket);
break;
}
Entity itemFrame = ItemFrameEntity.getItemFrameEntity(session, vector);
if (itemFrame != null) {
ServerboundInteractPacket interactPacket = new ServerboundInteractPacket(itemFrame.getEntityId(),
InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
session.sendDownstreamGamePacket(interactPacket);
}
}
}
}
}

View file

@ -0,0 +1,205 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.PlayerActionType;
import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.ItemUseTransaction;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.level.block.type.Block;
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.protocol.bedrock.BedrockInventoryTransactionTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractAction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import java.util.Set;
@Translator(packet = PlayerAuthInputPacket.class)
public class BedrockPlayerAuthInputTranslator extends PacketTranslator<PlayerAuthInputPacket> {
@Override
public void translate(GeyserSession session, PlayerAuthInputPacket packet) {
SessionPlayerEntity entity = session.getPlayerEntity();
session.getInputCache().processInputs(packet);
BedrockMovePlayerTranslator.translate(session, packet);
Set<PlayerAuthInputData> inputData = packet.getInputData();
if (!inputData.isEmpty()) {
for (PlayerAuthInputData input : inputData) {
switch (input) {
case PERFORM_ITEM_INTERACTION -> processItemUseTransaction(session, packet.getItemUseTransaction());
case PERFORM_BLOCK_ACTIONS -> BedrockBlockActions.translate(session, packet.getPlayerActions());
case START_SNEAKING -> {
ServerboundPlayerCommandPacket startSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
session.sendDownstreamGamePacket(startSneakPacket);
session.startSneaking();
}
case STOP_SNEAKING -> {
ServerboundPlayerCommandPacket stopSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SNEAKING);
session.sendDownstreamGamePacket(stopSneakPacket);
session.stopSneaking();
}
case START_SPRINTING -> {
if (!entity.getFlag(EntityFlag.SWIMMING)) {
ServerboundPlayerCommandPacket startSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING);
session.sendDownstreamGamePacket(startSprintPacket);
session.setSprinting(true);
}
}
case STOP_SPRINTING -> {
if (!entity.getFlag(EntityFlag.SWIMMING)) {
ServerboundPlayerCommandPacket stopSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SPRINTING);
session.sendDownstreamGamePacket(stopSprintPacket);
}
session.setSprinting(false);
}
case START_SWIMMING -> session.setSwimming(true);
case STOP_SWIMMING -> session.setSwimming(false);
case START_FLYING -> { // Since 1.20.30
if (session.isCanFly()) {
if (session.getGameMode() == GameMode.SPECTATOR) {
// should already be flying
session.sendAdventureSettings();
break;
}
if (session.getPlayerEntity().getFlag(EntityFlag.SWIMMING) && session.getCollisionManager().isPlayerInWater()) {
// As of 1.18.1, Java Edition cannot fly while in water, but it can fly while crawling
// If this isn't present, swimming on a 1.13.2 server and then attempting to fly will put you into a flying/swimming state that is invalid on JE
session.sendAdventureSettings();
break;
}
session.setFlying(true);
session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(true));
} else {
// update whether we can fly
session.sendAdventureSettings();
// stop flying
PlayerActionPacket stopFlyingPacket = new PlayerActionPacket();
stopFlyingPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
stopFlyingPacket.setAction(PlayerActionType.STOP_FLYING);
stopFlyingPacket.setBlockPosition(Vector3i.ZERO);
stopFlyingPacket.setResultPosition(Vector3i.ZERO);
stopFlyingPacket.setFace(0);
session.sendUpstreamPacket(stopFlyingPacket);
}
}
case STOP_FLYING -> {
session.setFlying(false);
session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(false));
}
case START_GLIDING -> {
// Otherwise gliding will not work in creative
ServerboundPlayerAbilitiesPacket playerAbilitiesPacket = new ServerboundPlayerAbilitiesPacket(false);
session.sendDownstreamGamePacket(playerAbilitiesPacket);
sendPlayerGlideToggle(session, entity);
}
case STOP_GLIDING -> sendPlayerGlideToggle(session, entity);
}
}
}
}
private static void sendPlayerGlideToggle(GeyserSession session, Entity entity) {
ServerboundPlayerCommandPacket glidePacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_ELYTRA_FLYING);
session.sendDownstreamGamePacket(glidePacket);
}
private static void processItemUseTransaction(GeyserSession session, ItemUseTransaction transaction) {
if (transaction.getActionType() == 2) {
int blockState = session.getGameMode() == GameMode.CREATIVE ?
session.getGeyser().getWorldManager().getBlockAt(session, transaction.getBlockPosition()) : session.getBreakingBlock();
session.setLastBlockPlaced(null);
session.setLastBlockPlacePosition(null);
// Same deal with vanilla block placing as above.
if (!session.getWorldBorder().isInsideBorderBoundaries()) {
BedrockInventoryTransactionTranslator.restoreCorrectBlock(session, transaction.getBlockPosition());
return;
}
Vector3f playerPosition = session.getPlayerEntity().getPosition();
playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
if (!BedrockInventoryTransactionTranslator.canInteractWithBlock(session, playerPosition, transaction.getBlockPosition())) {
BedrockInventoryTransactionTranslator.restoreCorrectBlock(session, transaction.getBlockPosition());
return;
}
int sequence = session.getWorldCache().nextPredictionSequence();
session.getWorldCache().markPositionInSequence(transaction.getBlockPosition());
// -1 means we don't know what block they're breaking
if (blockState == -1) {
blockState = Block.JAVA_AIR_ID;
}
LevelEventPacket blockBreakPacket = new LevelEventPacket();
blockBreakPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK);
blockBreakPacket.setPosition(transaction.getBlockPosition().toFloat());
blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
session.sendUpstreamPacket(blockBreakPacket);
session.setBreakingBlock(-1);
Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, transaction.getBlockPosition());
if (itemFrameEntity != null) {
ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(),
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamGamePacket(attackPacket);
return;
}
PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, transaction.getBlockPosition(), Direction.VALUES[transaction.getBlockFace()], sequence);
session.sendDownstreamGamePacket(breakPacket);
} else {
session.getGeyser().getLogger().error("Unhandled item use transaction type!");
}
}
}

View file

@ -25,6 +25,7 @@
package org.geysermc.geyser.translator.protocol.java;
import com.google.common.collect.Lists;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
@ -32,6 +33,7 @@ import net.kyori.adventure.key.Key;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.RecipeUnlockingRequirement;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescriptor;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
@ -52,6 +54,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.RecipeDispla
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.RecipeDisplayEntry;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapedCraftingRecipeDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapelessCraftingRecipeDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.SmithingRecipeDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.CompositeSlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay;
import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay;
@ -61,11 +64,12 @@ import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.TagSlot
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundRecipeBookAddPacket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@Translator(packet = ClientboundRecipeBookAddPacket.class)
@ -104,17 +108,17 @@ public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRec
boolean empty = true;
boolean complexInputs = false;
List<ItemDescriptorWithCount[]> inputs = new ArrayList<>(shapedRecipe.ingredients().size());
List<List<ItemDescriptorWithCount>> inputs = new ArrayList<>(shapedRecipe.ingredients().size());
for (SlotDisplay input : shapedRecipe.ingredients()) {
ItemDescriptorWithCount[] translated = translateToInput(session, input);
List<ItemDescriptorWithCount> translated = translateToInput(session, input);
if (translated == null) {
continue;
}
inputs.add(translated);
if (translated.length != 1 || translated[0] != ItemDescriptorWithCount.EMPTY) {
if (translated.size() != 1 || translated.get(0) != ItemDescriptorWithCount.EMPTY) {
empty = false;
}
complexInputs |= translated.length > 1;
complexInputs |= translated.size() > 1;
}
if (empty) {
// Crashes Bedrock 1.19.70 otherwise
@ -123,15 +127,31 @@ public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRec
}
if (complexInputs) {
} else {
String recipeId = Integer.toString(contents.id());
craftingDataPacket.getCraftingData().add(ShapedRecipeData.shaped(recipeId,
shapedRecipe.width(), shapedRecipe.height(), inputs.stream().map(descriptors -> descriptors[0]).toList(),
Collections.singletonList(output), UUID.randomUUID(), "crafting_table", 0, netId++, false, RecipeUnlockingRequirement.INVALID));
recipesPacket.getUnlockedRecipes().add(recipeId);
javaToBedrockRecipeIds.put(contents.id(), Collections.singletonList(recipeId));
System.out.println(inputs);
if (true) continue;
List<List<ItemDescriptorWithCount>> processedInputs = Lists.cartesianProduct(inputs);
System.out.println(processedInputs.size());
if (processedInputs.size() <= 500) { // Do not let us process giant lists.
List<String> bedrockRecipeIds = new ArrayList<>();
for (int i = 0; i < processedInputs.size(); i++) {
List<ItemDescriptorWithCount> possibleInput = processedInputs.get(i);
String recipeId = contents.id() + "_" + i;
craftingDataPacket.getCraftingData().add(ShapedRecipeData.shaped(recipeId,
shapedRecipe.width(), shapedRecipe.height(), possibleInput,
Collections.singletonList(output), UUID.randomUUID(), "crafting_table", 0, netId++, false, RecipeUnlockingRequirement.INVALID));
recipesPacket.getUnlockedRecipes().add(recipeId);
bedrockRecipeIds.add(recipeId);
}
javaToBedrockRecipeIds.put(contents.id(), bedrockRecipeIds);
continue;
}
}
String recipeId = Integer.toString(contents.id());
craftingDataPacket.getCraftingData().add(ShapedRecipeData.shaped(recipeId,
shapedRecipe.width(), shapedRecipe.height(), inputs.stream().map(descriptors -> descriptors.get(0)).toList(),
Collections.singletonList(output), UUID.randomUUID(), "crafting_table", 0, netId++, false, RecipeUnlockingRequirement.INVALID));
recipesPacket.getUnlockedRecipes().add(recipeId);
javaToBedrockRecipeIds.put(contents.id(), Collections.singletonList(recipeId));
}
case CRAFTING_SHAPELESS -> {
ShapelessCraftingRecipeDisplay shapelessRecipe = (ShapelessCraftingRecipeDisplay) display;
@ -147,6 +167,42 @@ public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRec
output = output.toBuilder().tag(null).build();
}
}
case SMITHING -> {
if (true) {
System.out.println(display);
continue;
}
SmithingRecipeDisplay smithingRecipe = (SmithingRecipeDisplay) display;
Pair<Item, ItemData> output = translateToOutput(session, smithingRecipe.result());
if (output == null) {
continue;
}
List<ItemDescriptorWithCount> bases = translateToInput(session, smithingRecipe.base());
List<ItemDescriptorWithCount> templates = translateToInput(session, smithingRecipe.template());
List<ItemDescriptorWithCount> additions = translateToInput(session, smithingRecipe.addition());
if (bases == null || templates == null || additions == null) {
continue;
}
int i = 0;
List<String> bedrockRecipeIds = new ArrayList<>();
for (ItemDescriptorWithCount template : templates) {
for (ItemDescriptorWithCount base : bases) {
for (ItemDescriptorWithCount addition : additions) {
String id = contents.id() + "_" + i++;
// Note: vanilla inputs use aux value of Short.MAX_VALUE
craftingDataPacket.getCraftingData().add(SmithingTransformRecipeData.of(id,
template, base, addition, output.right(), "smithing_table", netId++));
recipesPacket.getUnlockedRecipes().add(id);
bedrockRecipeIds.add(id);
}
}
}
javaToBedrockRecipeIds.put(contents.id(), bedrockRecipeIds);
}
}
}
@ -159,11 +215,11 @@ public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRec
TAG_TO_ITEM_DESCRIPTOR_CACHE.remove();
}
private static final ThreadLocal<Map<int[], ItemDescriptorWithCount[]>> TAG_TO_ITEM_DESCRIPTOR_CACHE = ThreadLocal.withInitial(Object2ObjectOpenHashMap::new);
private static final ThreadLocal<Map<int[], List<ItemDescriptorWithCount>>> TAG_TO_ITEM_DESCRIPTOR_CACHE = ThreadLocal.withInitial(Object2ObjectOpenHashMap::new);
private ItemDescriptorWithCount[] translateToInput(GeyserSession session, SlotDisplay slotDisplay) {
private List<ItemDescriptorWithCount> translateToInput(GeyserSession session, SlotDisplay slotDisplay) {
if (slotDisplay instanceof EmptySlotDisplay) {
return new ItemDescriptorWithCount[] {ItemDescriptorWithCount.EMPTY};
return Collections.singletonList(ItemDescriptorWithCount.EMPTY);
}
if (slotDisplay instanceof CompositeSlotDisplay composite) {
if (composite.contents().size() == 1) {
@ -172,23 +228,23 @@ public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRec
return composite.contents().stream()
.map(subDisplay -> translateToInput(session, subDisplay))
.filter(Objects::nonNull)
.flatMap(Arrays::stream)
.toArray(ItemDescriptorWithCount[]::new);
.flatMap(List::stream)
.toList();
}
if (slotDisplay instanceof ItemSlotDisplay itemSlot) {
return new ItemDescriptorWithCount[] {fromItem(session, itemSlot.item())};
return Collections.singletonList(fromItem(session, itemSlot.item()));
}
if (slotDisplay instanceof ItemStackSlotDisplay itemStackSlot) {
ItemData item = ItemTranslator.translateToBedrock(session, itemStackSlot.itemStack());
return new ItemDescriptorWithCount[] {ItemDescriptorWithCount.fromItem(item)};
return Collections.singletonList(ItemDescriptorWithCount.fromItem(item));
}
if (slotDisplay instanceof TagSlotDisplay tagSlot) {
Key tag = tagSlot.tag();
int[] items = session.getTagCache().getRaw(new Tag<>(JavaRegistries.ITEM, tag)); // I don't like this...
if (items == null || items.length == 0) {
return new ItemDescriptorWithCount[] {ItemDescriptorWithCount.EMPTY};
return Collections.singletonList(ItemDescriptorWithCount.EMPTY);
} else if (items.length == 1) {
return new ItemDescriptorWithCount[] {fromItem(session, items[0])};
return Collections.singletonList(fromItem(session, items[0]));
} else {
// Cache is implemented as, presumably, an item tag will be used multiple times in succession
// (E.G. a chest with planks tags)
@ -205,14 +261,14 @@ public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRec
// }).collect(Collectors.joining(" || "));
// if ("minecraft:planks".equals(tag.toString())) {
// String molang = "q.any_tag('minecraft:planks')";
// return new ItemDescriptorWithCount[] {new ItemDescriptorWithCount(new MolangDescriptor(molang, 10), 1)};
// return Collections.singletonList(new ItemDescriptorWithCount(new MolangDescriptor(molang, 10), 1));
// }
return null;
// Set<ItemDescriptorWithCount> itemDescriptors = new HashSet<>();
// for (int item : key) {
// itemDescriptors.add(fromItem(session, item));
// }
// return itemDescriptors.toArray(ItemDescriptorWithCount[]::new);
Set<ItemDescriptorWithCount> itemDescriptors = new HashSet<>();
for (int item : key) {
itemDescriptors.add(fromItem(session, item));
}
return new ArrayList<>(itemDescriptors); // This, or a list from the start with contains -> add?
});
}
}
@ -243,40 +299,4 @@ public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRec
ItemMapping mapping = session.getItemMappings().getMapping(item);
return new ItemDescriptorWithCount(new DefaultDescriptor(mapping.getBedrockDefinition(), mapping.getBedrockData()), 1); // Need to check count
}
// private static ItemDescriptorWithCount[][] combinations(ItemDescriptorWithCount[] itemDescriptors) {
// int totalCombinations = 1;
// for (Set<ItemDescriptorWithCount> optionSet : squashedOptions.keySet()) {
// totalCombinations *= optionSet.size();
// }
// if (totalCombinations > 500) {
// ItemDescriptorWithCount[] translatedItems = new ItemDescriptorWithCount[ingredients.length];
// for (int i = 0; i < ingredients.length; i++) {
// if (ingredients[i].getOptions().length > 0) {
// translatedItems[i] = ItemDescriptorWithCount.fromItem(ItemTranslator.translateToBedrock(session, ingredients[i].getOptions()[0]));
// } else {
// translatedItems[i] = ItemDescriptorWithCount.EMPTY;
// }
// }
// return new ItemDescriptorWithCount[][]{translatedItems};
// }
// List<Set<ItemDescriptorWithCount>> sortedSets = new ArrayList<>(squashedOptions.keySet());
// sortedSets.sort(Comparator.comparing(Set::size, Comparator.reverseOrder()));
// ItemDescriptorWithCount[][] combinations = new ItemDescriptorWithCount[totalCombinations][ingredients.length];
// int x = 1;
// for (Set<ItemDescriptorWithCount> set : sortedSets) {
// IntSet slotSet = squashedOptions.get(set);
// int i = 0;
// for (ItemDescriptorWithCount item : set) {
// for (int j = 0; j < totalCombinations / set.size(); j++) {
// final int comboIndex = (i * x) + (j % x) + ((j / x) * set.size() * x);
// for (IntIterator it = slotSet.iterator(); it.hasNext(); ) {
// combinations[comboIndex][it.nextInt()] = item;
// }
// }
// i++;
// }
// x *= set.size();
// }
// }
}

View file

@ -154,10 +154,9 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
int recipeNetId = netId++;
UUID uuid = UUID.randomUUID();
// We need to register stonecutting recipes, so they show up on Bedrock
// (Implementation note: recipe ID creates the order which stonecutting recipes are shown in stonecutter
// (Implementation note: recipe ID creates the order which stonecutting recipes are shown in stonecutter)
craftingDataPacket.getCraftingData().add(ShapelessRecipeData.shapeless("stonecutter_" + javaInput + "_" + buttonId,
Collections.singletonList(descriptor), Collections.singletonList(output), uuid, "stonecutter", 0, recipeNetId, RecipeUnlockingRequirement.INVALID));
session.getGeyser().getLogger().info(mapping.getJavaItem().javaIdentifier() + " " + buttonId + " " + recipeNetId);
// Save the recipe list for reference when crafting
// Add the net ID as the key and the button required + output for the value