Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;

public class AbstractArrowEntity extends Entity {
public class AbstractArrowEntity extends ThrowableEntity {

public AbstractArrowEntity(EntitySpawnContext context) {
super(context);
Expand Down Expand Up @@ -62,6 +62,16 @@ public void setPitch(float pitch) {
public void setHeadYaw(float headYaw) {
}

@Override
protected float getGravity() {
return getFlag(EntityFlag.HAS_GRAVITY) ? 0.05f : 0f;
}

@Override
protected float getDrag() {
return isInWater() ? 0.6f : 0.99f;
}

@Override
public void setMotion(Vector3f motion) {
super.setMotion(motion);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,13 @@ public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) {

@Override
public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) {
boolean serverUsingItem = (entityMetadata.getPrimitiveValue() & 0x01) == 0x01;
super.setLivingEntityFlags(entityMetadata);

if (!serverUsingItem && session.shouldIgnoreServerUsingItemFalse()) {
setFlag(EntityFlag.USING_ITEM, true);
}

// Forcefully update flags since we're not tracking thing like using item properly.
// For eg: when player start using item client-sided (and the USING_ITEM flag is false on geyser side)
// If the server disagree with the player using item state, it will send a metadata set USING_ITEM flag to false
Expand Down
46 changes: 46 additions & 0 deletions core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.HandPreference;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
import org.geysermc.mcprotocollib.protocol.data.game.setting.ChatVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.setting.ParticleStatus;
Expand Down Expand Up @@ -257,6 +258,8 @@
@Getter
public class GeyserSession implements GeyserConnection, GeyserCommandSource {

private static final long USING_ITEM_PREDICTION_GRACE_MILLIS = 250L;

private final GeyserImpl geyser;
private final UpstreamSession upstream;
private DownstreamSession downstream;
Expand Down Expand Up @@ -624,6 +627,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
*/
private int armAnimationTicks = -1;

/**
* Small grace window to ignore stale Java metadata that clears USING_ITEM too early.
*/
private long usingItemPredictionUntil;

/**
* The tick in which the player last hit air.
* Used to ensure we dont send two sing packets for one hit.
Expand Down Expand Up @@ -1441,6 +1449,12 @@ public void useItem(Hand hand, boolean useTouchRotation) {
return;
}

if (shouldPredictUsingItem(hand)) {
playerEntity.setFlag(EntityFlag.USING_ITEM, true);
playerEntity.updateBedrockMetadata();
beginUsingItemPrediction();
}

float yaw = playerEntity.getJavaYaw(), pitch = playerEntity.getPitch();
if (useTouchRotation) { // Only use touch rotation when we actually needed to, resolve https://github.com/GeyserMC/Geyser/issues/5704
yaw = playerEntity.getBedrockInteractRotation().getY();
Expand All @@ -1451,12 +1465,43 @@ public void useItem(Hand hand, boolean useTouchRotation) {
}

public void releaseItem() {
clearUsingItemPrediction();
// Followed to the Minecraft Protocol specification outlined at wiki.vg
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
Direction.DOWN, 0);
sendDownstreamGamePacket(releaseItemPacket);
}

private boolean shouldPredictUsingItem(Hand hand) {
var itemInHand = playerInventoryHolder.inventory().getItemInHand(hand);
if (itemInHand.isEmpty()) {
return false;
}

if (itemInHand.getComponent(DataComponentTypes.CONSUMABLE) != null) {
return true;
}

return itemInHand.is(Items.BOW)
|| itemInHand.is(Items.CROSSBOW)
|| itemInHand.is(Items.TRIDENT)
|| itemInHand.is(Items.SPYGLASS)
|| itemInHand.is(Items.GOAT_HORN)
|| itemInHand.is(Items.BRUSH);
}

public void beginUsingItemPrediction() {
usingItemPredictionUntil = System.currentTimeMillis() + USING_ITEM_PREDICTION_GRACE_MILLIS;
}

public void clearUsingItemPrediction() {
usingItemPredictionUntil = 0L;
}

public boolean shouldIgnoreServerUsingItemFalse() {
return playerEntity.getFlag(EntityFlag.USING_ITEM) && System.currentTimeMillis() < usingItemPredictionUntil;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldIgnoreServerUsingItemFalse() checks playerEntity.getFlag(EntityFlag.USING_ITEM), but SessionPlayerEntity#setLivingEntityFlags calls this after super.setLivingEntityFlags(entityMetadata) has already applied the server value (clearing USING_ITEM when the stale metadata is false). That makes this method return false in the exact scenario it's meant to handle, so the stale USING_ITEM=false update will not be ignored.

Consider basing this purely on the prediction window (e.g., System.currentTimeMillis() < usingItemPredictionUntil) and/or tracking a separate “prediction active” boolean that isn't overwritten by the server metadata application.

Suggested change
return playerEntity.getFlag(EntityFlag.USING_ITEM) && System.currentTimeMillis() < usingItemPredictionUntil;
return System.currentTimeMillis() < usingItemPredictionUntil;

Copilot uses AI. Check for mistakes.
}

/**
* Checks to see if a shield is in either hand to activate blocking. If so, it sets the Bedrock client to display
* blocking and sends a packet to the Java server.
Expand Down Expand Up @@ -1504,6 +1549,7 @@ public boolean isHandsBusy() {
*/
private boolean disableBlocking() {
if (playerEntity.getFlag(EntityFlag.BLOCKING)) {
clearUsingItemPrediction();
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM,
Vector3i.ZERO, Direction.DOWN, 0);
sendDownstreamGamePacket(releaseItemPacket);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public void translate(GeyserSession session, ClientboundEntityEventPacket packet
break;
case PLAYER_FINISH_USING_ITEM:
if (entity instanceof SessionPlayerEntity) {
session.clearUsingItemPrediction();
entity.setFlag(EntityFlag.USING_ITEM, false);
}

Expand Down
Loading