From e17ad64d8ca4e5e9b4eff351baee0ebcd0388043 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:47:03 -0400 Subject: [PATCH] Migrate to SERVER-AUTHORITATIVE MOVEMENT dun dun dunnnn --- .../level/physics/CollisionManager.java | 14 +- .../geyser/level/physics/CollisionResult.java | 3 +- .../geyser/network/CodecProcessor.java | 3 +- .../geyser/network/UpstreamPacketHandler.java | 2 +- .../geyser/session/GeyserSession.java | 24 +- .../geyser/session/cache/InputCache.java | 80 +++++++ ...BedrockInventoryTransactionTranslator.java | 73 ++----- ...anslator.java => BedrockBlockActions.java} | 181 ++-------------- .../player/BedrockMovePlayerTranslator.java | 51 +++-- .../player/BedrockPlayerActionTranslator.java | 108 +++++++++ .../BedrockPlayerAuthInputTranslator.java | 205 ++++++++++++++++++ .../java/JavaRecipeBookAddTranslator.java | 150 +++++++------ .../java/JavaUpdateRecipesTranslator.java | 3 +- 13 files changed, 557 insertions(+), 340 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/session/cache/InputCache.java rename core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/{BedrockActionTranslator.java => BedrockBlockActions.java} (58%) create mode 100644 core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockPlayerActionTranslator.java create mode 100644 core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockPlayerAuthInputTranslator.java diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java index a0fb312b4..5c87993df 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java @@ -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 diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionResult.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionResult.java index 8ba5f895b..3c8271cd9 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionResult.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionResult.java @@ -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) { } diff --git a/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java b/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java index cdbeef143..d5a4dd246 100644 --- a/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java +++ b/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java @@ -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) diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 48f1dee5f..19e56c8a8 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -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); diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index df5c3d7a8..9b9e86bb2 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -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); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/InputCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/InputCache.java new file mode 100644 index 000000000..b59df0c16 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/InputCache.java @@ -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 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; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 7ed4ac72c..421e082b1 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -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 false; }; if (isGodBridging) { - restoreCorrectBlock(session, blockPos, packet); + restoreCorrectBlock(session, blockPos); return; } } @@ -207,7 +205,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { - 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 { +import java.util.List; - @Override - public void translate(GeyserSession session, PlayerActionPacket packet) { +final class BedrockBlockActions { + + static void translate(GeyserSession session, List 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 { // 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 { - @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 { + + @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); + } + } + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockPlayerAuthInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockPlayerAuthInputTranslator.java new file mode 100644 index 000000000..5465de51c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockPlayerAuthInputTranslator.java @@ -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 { + + @Override + public void translate(GeyserSession session, PlayerAuthInputPacket packet) { + SessionPlayerEntity entity = session.getPlayerEntity(); + session.getInputCache().processInputs(packet); + + BedrockMovePlayerTranslator.translate(session, packet); + + Set 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!"); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java index 49c62989c..9cb238c71 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java @@ -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 inputs = new ArrayList<>(shapedRecipe.ingredients().size()); + List> inputs = new ArrayList<>(shapedRecipe.ingredients().size()); for (SlotDisplay input : shapedRecipe.ingredients()) { - ItemDescriptorWithCount[] translated = translateToInput(session, input); + List 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 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> processedInputs = Lists.cartesianProduct(inputs); + System.out.println(processedInputs.size()); + if (processedInputs.size() <= 500) { // Do not let us process giant lists. + List bedrockRecipeIds = new ArrayList<>(); + for (int i = 0; i < processedInputs.size(); i++) { + List 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 { + if (true) { + System.out.println(display); + continue; + } + SmithingRecipeDisplay smithingRecipe = (SmithingRecipeDisplay) display; + Pair output = translateToOutput(session, smithingRecipe.result()); + if (output == null) { + continue; + } + + List bases = translateToInput(session, smithingRecipe.base()); + List templates = translateToInput(session, smithingRecipe.template()); + List additions = translateToInput(session, smithingRecipe.addition()); + + if (bases == null || templates == null || additions == null) { + continue; + } + + int i = 0; + List 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> TAG_TO_ITEM_DESCRIPTOR_CACHE = ThreadLocal.withInitial(Object2ObjectOpenHashMap::new); + private static final ThreadLocal>> TAG_TO_ITEM_DESCRIPTOR_CACHE = ThreadLocal.withInitial(Object2ObjectOpenHashMap::new); - private ItemDescriptorWithCount[] translateToInput(GeyserSession session, SlotDisplay slotDisplay) { + private List 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 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 itemDescriptors = new HashSet<>(); -// for (int item : key) { -// itemDescriptors.add(fromItem(session, item)); -// } -// return itemDescriptors.toArray(ItemDescriptorWithCount[]::new); + + Set 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 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> 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 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(); -// } -// } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index 729ff965b..de62fb922 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -154,10 +154,9 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator