Skip to content

Commit 52bcdf0

Browse files
Fix: Allow players to hit air (#5369)
This also implements blocking block breaking & attacking entities while steering boats to match the Java clients behavior. Further, it now also updates the shifting state before sending inputs to the Java client, also matching behavior there.
1 parent 97cc876 commit 52bcdf0

File tree

11 files changed

+109
-60
lines changed

11 files changed

+109
-60
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
1515
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
1616

1717
## Supported Versions
18-
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/).
18+
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/).
1919

2020
## Setting Up
2121
Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser.

core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,8 @@ public static class NetworkInfo {
178178

179179
NetworkInfo() {
180180
if (AsteriskSerializer.showSensitive) {
181-
try {
181+
try (Socket socket = new Socket()) {
182182
// This is the most reliable for getting the main local IP
183-
Socket socket = new Socket();
184183
socket.connect(new InetSocketAddress("geysermc.org", 80));
185184
this.internalIP = socket.getLocalAddress().getHostAddress();
186185
} catch (IOException e1) {

core/src/main/java/org/geysermc/geyser/session/GeyserSession.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -536,10 +536,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
536536

537537
/**
538538
* Counts how many ticks have occurred since an arm animation started.
539-
* -1 means there is no active arm swing; -2 means an arm swing will start in a tick.
539+
* -1 means there is no active arm swing
540540
*/
541541
private int armAnimationTicks = -1;
542542

543+
/**
544+
* The tick in which the player last hit air.
545+
* Used to ensure we dont send two sing packets for one hit.
546+
*/
547+
@Setter
548+
private int lastAirHitTick;
549+
543550
/**
544551
* Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless.
545552
*/
@@ -1113,13 +1120,10 @@ public ScheduledFuture<?> scheduleInEventLoop(Runnable runnable, long duration,
11131120

11141121
public void updateTickingState(float tickRate, boolean frozen) {
11151122
tickThread.cancel(false);
1116-
11171123
this.tickingFrozen = frozen;
11181124

11191125
tickRate = MathUtils.clamp(tickRate, 1.0f, 10000.0f);
1120-
11211126
millisecondsPerTick = 1000.0f / tickRate;
1122-
11231127
nanosecondsPerTick = MathUtils.ceil(1000000000.0f / tickRate);
11241128
tickThread = tickEventLoop.scheduleAtFixedRate(this::tick, nanosecondsPerTick, nanosecondsPerTick, TimeUnit.NANOSECONDS);
11251129
}
@@ -1132,7 +1136,6 @@ private void executeRunnable(Runnable runnable) {
11321136
} catch (Throwable e) {
11331137
geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e);
11341138
}
1135-
11361139
}
11371140

11381141
/**
@@ -1365,13 +1368,10 @@ public void activateArmAnimationTicking() {
13651368
}
13661369

13671370
/**
1368-
* For <a href="https://github.com/GeyserMC/Geyser/issues/2113">issue 2113</a> and combating arm ticking activating being delayed in
1369-
* BedrockAnimateTranslator.
1371+
* You can't break blocks, attack entities, or use items while driving in a boat
13701372
*/
1371-
public void armSwingPending() {
1372-
if (armAnimationTicks == -1) {
1373-
armAnimationTicks = -2;
1374-
}
1373+
public boolean isHandsBusy() {
1374+
return steeringRight || steeringLeft;
13751375
}
13761376

13771377
/**

core/src/main/java/org/geysermc/geyser/session/cache/InputCache.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@
3232
import org.cloudburstmc.protocol.bedrock.data.InputMode;
3333
import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData;
3434
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
35+
import org.geysermc.geyser.entity.type.player.PlayerEntity;
3536
import org.geysermc.geyser.session.GeyserSession;
37+
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState;
3638
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundPlayerInputPacket;
39+
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
3740

3841
import java.util.Set;
3942

@@ -53,7 +56,7 @@ public InputCache(GeyserSession session) {
5356
this.session = session;
5457
}
5558

56-
public void processInputs(PlayerAuthInputPacket packet) {
59+
public void processInputs(PlayerEntity entity, PlayerAuthInputPacket packet) {
5760
// Input is sent to the server before packet positions, as of 1.21.2
5861
Set<PlayerAuthInputData> bedrockInput = packet.getInputData();
5962
var oldInputPacket = this.inputPacket;
@@ -74,16 +77,29 @@ public void processInputs(PlayerAuthInputPacket packet) {
7477
right = analogMovement.getX() < 0;
7578
}
7679

80+
boolean sneaking = bedrockInput.contains(PlayerAuthInputData.SNEAKING);
81+
7782
// TODO when is UP_LEFT, etc. used?
7883
this.inputPacket = this.inputPacket
7984
.withForward(up)
8085
.withBackward(down)
8186
.withLeft(left)
8287
.withRight(right)
8388
.withJump(bedrockInput.contains(PlayerAuthInputData.JUMPING)) // Looks like this only triggers when the JUMP key input is being pressed. There's also JUMP_DOWN?
84-
.withShift(bedrockInput.contains(PlayerAuthInputData.SNEAKING))
89+
.withShift(sneaking)
8590
.withSprint(bedrockInput.contains(PlayerAuthInputData.SPRINTING)); // SPRINTING will trigger even if the player isn't moving
8691

92+
// Send sneaking state before inputs, matches Java client
93+
if (oldInputPacket.isShift() != sneaking) {
94+
if (sneaking) {
95+
session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.START_SNEAKING));
96+
session.startSneaking();
97+
} else {
98+
session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.STOP_SNEAKING));
99+
session.stopSneaking();
100+
}
101+
}
102+
87103
if (oldInputPacket != this.inputPacket) { // Simple equality check is fine since we're checking for an instance change.
88104
session.sendDownstreamGamePacket(this.inputPacket);
89105
}

core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BrushableBlockEntityTranslator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, @Nulla
6464
}
6565
NbtMapBuilder itemBuilder = NbtMap.builder()
6666
.putString("Name", mapping.getBedrockIdentifier())
67-
.putByte("Count", (byte) itemTag.getByte("Count"));
67+
.putByte("Count", itemTag.getByte("Count"));
6868

6969
bedrockNbt.putCompound("item", itemBuilder.build());
7070
// controls which side the item protrudes from

core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,22 @@ public void translate(GeyserSession session, AnimatePacket packet) {
4545
}
4646

4747
if (packet.getAction() == AnimatePacket.Action.SWING_ARM) {
48-
session.armSwingPending();
49-
// Delay so entity damage can be processed first
48+
49+
// If this is the case, we just hit the air. Poor air.
50+
// Touch devices send PlayerAuthInputPackets with MISSED_SWING, and then the animate packet.
51+
// This tends to happen 1-2 ticks after the auth input packet.
52+
if (session.getTicks() - session.getLastAirHitTick() < 3) {
53+
return;
54+
}
55+
56+
// Windows unfortunately sends the animate packet first, then the auth input packet with the MISSED_SWING.
57+
// Often, these are sent in the same tick. In that case, the wait here ensures the auth input packet is processed first.
58+
// Other times, there is a 1-tick-delay, which would result in the swing packet sent here. The BedrockAuthInputTranslator's
59+
// MISSED_SWING case also accounts for that by checking if a swing was sent a tick ago here.
60+
61+
// Also, delay the swing so entity damage can be processed first
5062
session.scheduleInEventLoop(() -> {
51-
if (session.getArmAnimationTicks() != 0) {
63+
if (session.getArmAnimationTicks() != 0 && (session.getTicks() - session.getLastAirHitTick() > 2)) {
5264
// So, generally, a Java player can only do one *thing* at a time.
5365
// If a player right-clicks, for example, then there's probably only one action associated with
5466
// that right-click that will send a swing.
@@ -61,12 +73,12 @@ public void translate(GeyserSession session, AnimatePacket packet) {
6173
// This behavior was last touched on with ViaVersion 4.5.1 (with its packet limiter), Java 1.16.5,
6274
// and Bedrock 1.19.51.
6375
// Note for the future: we should probably largely ignore this packet and instead replicate
64-
// all actions on our end, and send swings where needed.
76+
// all actions on our end, and send swings where needed. Can be done once we replicate Block and Item interactions fully.
6577
session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
6678
session.activateArmAnimationTicking();
6779
}
6880
},
69-
25,
81+
(long) (session.getMillisecondsPerTick() * 0.5),
7082
TimeUnit.MILLISECONDS
7183
);
7284
}

core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,11 @@ public void translate(GeyserSession session, InventoryTransactionPacket packet)
454454
switch (packet.getActionType()) {
455455
case 0 -> processEntityInteraction(session, packet, entity); // Interact
456456
case 1 -> { // Attack
457+
if (session.isHandsBusy()) {
458+
// See Minecraft#startAttack and LocalPlayer#isHandsBusy
459+
return;
460+
}
461+
457462
int entityId;
458463
if (entity.getDefinition() == EntityDefinitions.ENDER_DRAGON) {
459464
// Redirects the attack to its body entity, this only happens when

core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockBlockActions.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.cloudburstmc.protocol.bedrock.data.PlayerBlockActionData;
3333
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
3434
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
35-
import org.geysermc.geyser.GeyserImpl;
3635
import org.geysermc.geyser.api.block.custom.CustomBlockState;
3736
import org.geysermc.geyser.entity.type.Entity;
3837
import org.geysermc.geyser.entity.type.ItemFrameEntity;
@@ -84,6 +83,10 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
8483
break;
8584
}
8685

86+
if (!canMine(session, vector)) {
87+
return;
88+
}
89+
8790
// Start the block breaking animation
8891
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, vector);
8992
LevelEventPacket startBreak = new LevelEventPacket();
@@ -126,6 +129,11 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
126129
if (session.getGameMode() == GameMode.CREATIVE) {
127130
break;
128131
}
132+
133+
if (!canMine(session, vector)) {
134+
return;
135+
}
136+
129137
int breakingBlock = session.getBreakingBlock();
130138
if (breakingBlock == -1) {
131139
breakingBlock = Block.JAVA_AIR_ID;
@@ -187,6 +195,7 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
187195
stopBreak.setPosition(vector.toFloat());
188196
stopBreak.setData(0);
189197
session.setBreakingBlock(-1);
198+
session.setBlockBreakStartTime(0);
190199
session.sendUpstreamPacket(stopBreak);
191200
}
192201
// Handled in BedrockInventoryTransactionTranslator
@@ -195,6 +204,22 @@ private static void handle(GeyserSession session, PlayerBlockActionData blockAct
195204
}
196205
}
197206

207+
private static boolean canMine(GeyserSession session, Vector3i vector) {
208+
if (session.isHandsBusy()) {
209+
session.setBreakingBlock(-1);
210+
session.setBlockBreakStartTime(0);
211+
212+
LevelEventPacket stopBreak = new LevelEventPacket();
213+
stopBreak.setType(LevelEvent.BLOCK_STOP_BREAK);
214+
stopBreak.setPosition(vector.toFloat());
215+
stopBreak.setData(0);
216+
session.setBreakingBlock(-1);
217+
session.sendUpstreamPacket(stopBreak);
218+
return false;
219+
}
220+
return true;
221+
}
222+
198223
private static void spawnBlockBreakParticles(GeyserSession session, Direction direction, Vector3i position, BlockState blockState) {
199224
LevelEventPacket levelEventPacket = new LevelEventPacket();
200225
switch (direction) {

core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@
2929
import org.cloudburstmc.math.vector.Vector2f;
3030
import org.cloudburstmc.math.vector.Vector3f;
3131
import org.cloudburstmc.math.vector.Vector3i;
32+
import org.cloudburstmc.protocol.bedrock.data.InputMode;
3233
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
3334
import org.cloudburstmc.protocol.bedrock.data.PlayerActionType;
3435
import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData;
3536
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
3637
import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.ItemUseTransaction;
38+
import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket;
3739
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
3840
import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket;
3941
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
42+
import org.geysermc.geyser.GeyserImpl;
4043
import org.geysermc.geyser.entity.EntityDefinitions;
4144
import org.geysermc.geyser.entity.type.BoatEntity;
4245
import org.geysermc.geyser.entity.type.Entity;
@@ -53,6 +56,7 @@
5356
import org.geysermc.geyser.util.CooldownUtils;
5457
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
5558
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
59+
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
5660
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractAction;
5761
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
5862
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState;
@@ -61,6 +65,7 @@
6165
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
6266
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
6367
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
68+
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket;
6469

6570
import java.util.Set;
6671

@@ -72,7 +77,7 @@ public void translate(GeyserSession session, PlayerAuthInputPacket packet) {
7277
SessionPlayerEntity entity = session.getPlayerEntity();
7378

7479
boolean wasJumping = session.getInputCache().wasJumping();
75-
session.getInputCache().processInputs(packet);
80+
session.getInputCache().processInputs(entity, packet);
7681

7782
BedrockMovePlayer.translate(session, packet);
7883

@@ -83,18 +88,6 @@ public void translate(GeyserSession session, PlayerAuthInputPacket packet) {
8388
switch (input) {
8489
case PERFORM_ITEM_INTERACTION -> processItemUseTransaction(session, packet.getItemUseTransaction());
8590
case PERFORM_BLOCK_ACTIONS -> BedrockBlockActions.translate(session, packet.getPlayerActions());
86-
case START_SNEAKING -> {
87-
ServerboundPlayerCommandPacket startSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
88-
session.sendDownstreamGamePacket(startSneakPacket);
89-
90-
session.startSneaking();
91-
}
92-
case STOP_SNEAKING -> {
93-
ServerboundPlayerCommandPacket stopSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SNEAKING);
94-
session.sendDownstreamGamePacket(stopSneakPacket);
95-
96-
session.stopSneaking();
97-
}
9891
case START_SPRINTING -> {
9992
if (!entity.getFlag(EntityFlag.SWIMMING)) {
10093
ServerboundPlayerCommandPacket startSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING);
@@ -154,7 +147,25 @@ public void translate(GeyserSession session, PlayerAuthInputPacket packet) {
154147
sendPlayerGlideToggle(session, entity);
155148
}
156149
case STOP_GLIDING -> sendPlayerGlideToggle(session, entity);
157-
case MISSED_SWING -> CooldownUtils.sendCooldown(session); // Java edition sends a cooldown when hitting air.
150+
case MISSED_SWING -> {
151+
session.setLastAirHitTick(session.getTicks());
152+
153+
if (session.getArmAnimationTicks() != 0 && session.getArmAnimationTicks() != 1) {
154+
session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
155+
session.activateArmAnimationTicking();
156+
}
157+
158+
// Touch devices expect an animation packet sent back to them
159+
if (packet.getInputMode().equals(InputMode.TOUCH)) {
160+
AnimatePacket animatePacket = new AnimatePacket();
161+
animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
162+
animatePacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
163+
session.sendUpstreamPacket(animatePacket);
164+
}
165+
166+
// Java edition sends a cooldown when hitting air.
167+
CooldownUtils.sendCooldown(session);
168+
}
158169
}
159170
}
160171
if (entity.getVehicle() instanceof BoatEntity) {

core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.cloudburstmc.math.vector.Vector3f;
2929
import org.cloudburstmc.math.vector.Vector3i;
3030
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
31-
import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket;
3231
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
3332
import org.geysermc.geyser.level.block.property.Properties;
3433
import org.geysermc.geyser.level.block.type.BlockState;
@@ -38,7 +37,6 @@
3837
import org.geysermc.geyser.util.CooldownUtils;
3938
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
4039
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
41-
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket;
4240
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
4341

4442
@Translator(packet = LevelSoundEventPacket.class)
@@ -56,23 +54,6 @@ public void translate(GeyserSession session, LevelSoundEventPacket packet) {
5654
CooldownUtils.sendCooldown(session);
5755
}
5856

59-
if (packet.getSound() == SoundEvent.ATTACK_NODAMAGE && session.getArmAnimationTicks() == -1) {
60-
// https://github.com/GeyserMC/Geyser/issues/2113
61-
// Seems like consoles and Android with keyboard send the animation packet on 1.19.51, hence the animation
62-
// tick check - the animate packet is sent first.
63-
// ATTACK_NODAMAGE = player clicked air
64-
// This should only be revisited if Bedrock packets get full Java parity, or Bedrock starts sending arm
65-
// animation packets after ATTACK_NODAMAGE, OR ATTACK_NODAMAGE gets removed/isn't sent in the same spot
66-
session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
67-
session.activateArmAnimationTicking();
68-
69-
// Send packet to Bedrock so it knows
70-
AnimatePacket animatePacket = new AnimatePacket();
71-
animatePacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
72-
animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
73-
session.sendUpstreamPacket(animatePacket);
74-
}
75-
7657
// Used by client to get book from lecterns in survial mode since 1.20.70
7758
if (packet.getSound() == SoundEvent.HIT) {
7859
Vector3f position = packet.getPosition();

0 commit comments

Comments
 (0)