diff --git a/README.md b/README.md
index 57aaaf6fe..c6962f65e 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
## Supported Versions
-Geyser is currently supporting Minecraft Bedrock 1.21.40 - 1.21.60 and Minecraft Java 1.21.4. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
+Geyser is currently supporting Minecraft Bedrock 1.21.40 - 1.21.61 and Minecraft Java 1.21.4. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
## Setting Up
Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser.
diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
index 515e1a629..847eda133 100644
--- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
+++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
@@ -178,9 +178,8 @@ public class DumpInfo {
NetworkInfo() {
if (AsteriskSerializer.showSensitive) {
- try {
+ try (Socket socket = new Socket()) {
// This is the most reliable for getting the main local IP
- Socket socket = new Socket();
socket.connect(new InetSocketAddress("geysermc.org", 80));
this.internalIP = socket.getLocalAddress().getHostAddress();
} catch (IOException e1) {
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 261e57aed..96f553e20 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -536,10 +536,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* Counts how many ticks have occurred since an arm animation started.
- * -1 means there is no active arm swing; -2 means an arm swing will start in a tick.
+ * -1 means there is no active arm swing
*/
private int armAnimationTicks = -1;
+ /**
+ * The tick in which the player last hit air.
+ * Used to ensure we dont send two sing packets for one hit.
+ */
+ @Setter
+ private int lastAirHitTick;
+
/**
* Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless.
*/
@@ -1113,13 +1120,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
public void updateTickingState(float tickRate, boolean frozen) {
tickThread.cancel(false);
-
this.tickingFrozen = frozen;
tickRate = MathUtils.clamp(tickRate, 1.0f, 10000.0f);
-
millisecondsPerTick = 1000.0f / tickRate;
-
nanosecondsPerTick = MathUtils.ceil(1000000000.0f / tickRate);
tickThread = tickEventLoop.scheduleAtFixedRate(this::tick, nanosecondsPerTick, nanosecondsPerTick, TimeUnit.NANOSECONDS);
}
@@ -1132,7 +1136,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
} catch (Throwable e) {
geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e);
}
-
}
/**
@@ -1365,13 +1368,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
/**
- * For issue 2113 and combating arm ticking activating being delayed in
- * BedrockAnimateTranslator.
+ * You can't break blocks, attack entities, or use items while driving in a boat
*/
- public void armSwingPending() {
- if (armAnimationTicks == -1) {
- armAnimationTicks = -2;
- }
+ public boolean isHandsBusy() {
+ return steeringRight || steeringLeft;
}
/**
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
index f12c4d3c8..b6b0703d1 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/InputCache.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/InputCache.java
@@ -32,8 +32,11 @@ import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.protocol.bedrock.data.InputMode;
import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData;
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
+import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundPlayerInputPacket;
+import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import java.util.Set;
@@ -53,7 +56,7 @@ public final class InputCache {
this.session = session;
}
- public void processInputs(PlayerAuthInputPacket packet) {
+ public void processInputs(PlayerEntity entity, PlayerAuthInputPacket packet) {
// Input is sent to the server before packet positions, as of 1.21.2
Set bedrockInput = packet.getInputData();
var oldInputPacket = this.inputPacket;
@@ -74,6 +77,8 @@ public final class InputCache {
right = analogMovement.getX() < 0;
}
+ boolean sneaking = bedrockInput.contains(PlayerAuthInputData.SNEAKING);
+
// TODO when is UP_LEFT, etc. used?
this.inputPacket = this.inputPacket
.withForward(up)
@@ -81,9 +86,20 @@ public final class InputCache {
.withLeft(left)
.withRight(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))
+ .withShift(sneaking)
.withSprint(bedrockInput.contains(PlayerAuthInputData.SPRINTING)); // SPRINTING will trigger even if the player isn't moving
+ // Send sneaking state before inputs, matches Java client
+ if (oldInputPacket.isShift() != sneaking) {
+ if (sneaking) {
+ session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.START_SNEAKING));
+ session.startSneaking();
+ } else {
+ session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.STOP_SNEAKING));
+ session.stopSneaking();
+ }
+ }
+
if (oldInputPacket != this.inputPacket) { // Simple equality check is fine since we're checking for an instance change.
session.sendDownstreamGamePacket(this.inputPacket);
}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BrushableBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BrushableBlockEntityTranslator.java
index de0ce62b3..cc6d08647 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BrushableBlockEntityTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BrushableBlockEntityTranslator.java
@@ -64,7 +64,7 @@ public class BrushableBlockEntityTranslator extends BlockEntityTranslator implem
}
NbtMapBuilder itemBuilder = NbtMap.builder()
.putString("Name", mapping.getBedrockIdentifier())
- .putByte("Count", (byte) itemTag.getByte("Count"));
+ .putByte("Count", itemTag.getByte("Count"));
bedrockNbt.putCompound("item", itemBuilder.build());
// controls which side the item protrudes from
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java
index 5f3b9662f..04a641313 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java
@@ -45,10 +45,22 @@ public class BedrockAnimateTranslator extends PacketTranslator {
}
if (packet.getAction() == AnimatePacket.Action.SWING_ARM) {
- session.armSwingPending();
- // Delay so entity damage can be processed first
+
+ // If this is the case, we just hit the air. Poor air.
+ // Touch devices send PlayerAuthInputPackets with MISSED_SWING, and then the animate packet.
+ // This tends to happen 1-2 ticks after the auth input packet.
+ if (session.getTicks() - session.getLastAirHitTick() < 3) {
+ return;
+ }
+
+ // Windows unfortunately sends the animate packet first, then the auth input packet with the MISSED_SWING.
+ // Often, these are sent in the same tick. In that case, the wait here ensures the auth input packet is processed first.
+ // Other times, there is a 1-tick-delay, which would result in the swing packet sent here. The BedrockAuthInputTranslator's
+ // MISSED_SWING case also accounts for that by checking if a swing was sent a tick ago here.
+
+ // Also, delay the swing so entity damage can be processed first
session.scheduleInEventLoop(() -> {
- if (session.getArmAnimationTicks() != 0) {
+ if (session.getArmAnimationTicks() != 0 && (session.getTicks() - session.getLastAirHitTick() > 2)) {
// So, generally, a Java player can only do one *thing* at a time.
// If a player right-clicks, for example, then there's probably only one action associated with
// that right-click that will send a swing.
@@ -61,12 +73,12 @@ public class BedrockAnimateTranslator extends PacketTranslator {
// This behavior was last touched on with ViaVersion 4.5.1 (with its packet limiter), Java 1.16.5,
// and Bedrock 1.19.51.
// Note for the future: we should probably largely ignore this packet and instead replicate
- // all actions on our end, and send swings where needed.
+ // all actions on our end, and send swings where needed. Can be done once we replicate Block and Item interactions fully.
session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
session.activateArmAnimationTicking();
}
},
- 25,
+ (long) (session.getMillisecondsPerTick() * 0.5),
TimeUnit.MILLISECONDS
);
}
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 2a3d1eb57..f48b8b1c8 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
@@ -454,6 +454,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator processEntityInteraction(session, packet, entity); // Interact
case 1 -> { // Attack
+ if (session.isHandsBusy()) {
+ // See Minecraft#startAttack and LocalPlayer#isHandsBusy
+ return;
+ }
+
int entityId;
if (entity.getDefinition() == EntityDefinitions.ENDER_DRAGON) {
// Redirects the attack to its body entity, this only happens when
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockBlockActions.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockBlockActions.java
index c604f5be1..4473a1d60 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockBlockActions.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockBlockActions.java
@@ -32,7 +32,6 @@ 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.packet.LevelEventPacket;
-import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
@@ -84,6 +83,10 @@ final class BedrockBlockActions {
break;
}
+ if (!canMine(session, vector)) {
+ return;
+ }
+
// Start the block breaking animation
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, vector);
LevelEventPacket startBreak = new LevelEventPacket();
@@ -126,6 +129,11 @@ final class BedrockBlockActions {
if (session.getGameMode() == GameMode.CREATIVE) {
break;
}
+
+ if (!canMine(session, vector)) {
+ return;
+ }
+
int breakingBlock = session.getBreakingBlock();
if (breakingBlock == -1) {
breakingBlock = Block.JAVA_AIR_ID;
@@ -187,6 +195,7 @@ final class BedrockBlockActions {
stopBreak.setPosition(vector.toFloat());
stopBreak.setData(0);
session.setBreakingBlock(-1);
+ session.setBlockBreakStartTime(0);
session.sendUpstreamPacket(stopBreak);
}
// Handled in BedrockInventoryTransactionTranslator
@@ -195,6 +204,22 @@ final class BedrockBlockActions {
}
}
+ private static boolean canMine(GeyserSession session, Vector3i vector) {
+ if (session.isHandsBusy()) {
+ session.setBreakingBlock(-1);
+ session.setBlockBreakStartTime(0);
+
+ LevelEventPacket stopBreak = new LevelEventPacket();
+ stopBreak.setType(LevelEvent.BLOCK_STOP_BREAK);
+ stopBreak.setPosition(vector.toFloat());
+ stopBreak.setData(0);
+ session.setBreakingBlock(-1);
+ session.sendUpstreamPacket(stopBreak);
+ return false;
+ }
+ return true;
+ }
+
private static void spawnBlockBreakParticles(GeyserSession session, Direction direction, Vector3i position, BlockState blockState) {
LevelEventPacket levelEventPacket = new LevelEventPacket();
switch (direction) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java
index ee5a0dadb..62e574a3a 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java
@@ -29,14 +29,17 @@ import org.cloudburstmc.math.GenericMath;
import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
+import org.cloudburstmc.protocol.bedrock.data.InputMode;
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.AnimatePacket;
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.GeyserImpl;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.BoatEntity;
import org.geysermc.geyser.entity.type.Entity;
@@ -53,6 +56,7 @@ import org.geysermc.geyser.translator.protocol.bedrock.BedrockInventoryTransacti
import org.geysermc.geyser.util.CooldownUtils;
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.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;
@@ -61,6 +65,7 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.Serv
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 java.util.Set;
@@ -72,7 +77,7 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator 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);
@@ -154,7 +147,25 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator sendPlayerGlideToggle(session, entity);
- case MISSED_SWING -> CooldownUtils.sendCooldown(session); // Java edition sends a cooldown when hitting air.
+ case MISSED_SWING -> {
+ session.setLastAirHitTick(session.getTicks());
+
+ if (session.getArmAnimationTicks() != 0 && session.getArmAnimationTicks() != 1) {
+ session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
+ session.activateArmAnimationTicking();
+ }
+
+ // Touch devices expect an animation packet sent back to them
+ if (packet.getInputMode().equals(InputMode.TOUCH)) {
+ AnimatePacket animatePacket = new AnimatePacket();
+ animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
+ animatePacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
+ session.sendUpstreamPacket(animatePacket);
+ }
+
+ // Java edition sends a cooldown when hitting air.
+ CooldownUtils.sendCooldown(session);
+ }
}
}
if (entity.getVehicle() instanceof BoatEntity) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java
index 62b1b8b14..e99a31363 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java
@@ -28,7 +28,6 @@ package org.geysermc.geyser.translator.protocol.bedrock.world;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
-import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
import org.geysermc.geyser.level.block.property.Properties;
import org.geysermc.geyser.level.block.type.BlockState;
@@ -38,7 +37,6 @@ import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.CooldownUtils;
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.packet.ingame.serverbound.player.ServerboundSwingPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
@Translator(packet = LevelSoundEventPacket.class)
@@ -56,23 +54,6 @@ public class BedrockLevelSoundEventTranslator extends PacketTranslator