diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
index f8eafb5d7..33689dd54 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
@@ -171,7 +171,7 @@ public class Entity implements GeyserEntity {
dirtyMetadata.put(EntityDataTypes.SCALE, 1f);
dirtyMetadata.put(EntityDataTypes.COLOR, (byte) 0);
dirtyMetadata.put(EntityDataTypes.AIR_SUPPLY_MAX, getMaxAir());
- setDimensions(Pose.STANDING);
+ setDimensionsFromPose(Pose.STANDING);
setFlag(EntityFlag.HAS_GRAVITY, true);
setFlag(EntityFlag.HAS_COLLISION, true);
setFlag(EntityFlag.CAN_SHOW_NAME, true);
@@ -405,7 +405,7 @@ public class Entity implements GeyserEntity {
setFlag(EntityFlag.SPRINTING, (xd & 0x08) == 0x08);
// Swimming is ignored here and instead we rely on the pose
- setFlag(EntityFlag.GLIDING, (xd & 0x80) == 0x80);
+ setGliding((xd & 0x80) == 0x80);
setInvisible((xd & 0x20) == 0x20);
}
@@ -419,6 +419,13 @@ public class Entity implements GeyserEntity {
setFlag(EntityFlag.INVISIBLE, value);
}
+ /**
+ * Set a boolean - whether the entity is gliding
+ */
+ protected void setGliding(boolean value) {
+ setFlag(EntityFlag.GLIDING, value);
+ }
+
/**
* Set an int from 0 - this entity's maximum air - (air / maxAir) represents the percentage of bubbles left
*/
@@ -529,15 +536,16 @@ public class Entity implements GeyserEntity {
*/
public void setPose(Pose pose) {
setFlag(EntityFlag.SLEEPING, pose.equals(Pose.SLEEPING));
+ // FALL_FLYING is instead set via setFlags
// Triggered when crawling
setFlag(EntityFlag.SWIMMING, pose.equals(Pose.SWIMMING));
- setDimensions(pose);
+ setDimensionsFromPose(pose);
}
/**
* Set the height and width of the entity's bounding box
*/
- protected void setDimensions(Pose pose) {
+ protected void setDimensionsFromPose(Pose pose) {
// No flexibility options for basic entities
setBoundingBoxHeight(definition.height());
setBoundingBoxWidth(definition.width());
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java
index d7a9990fe..c938cfa8e 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java
@@ -27,9 +27,7 @@ package org.geysermc.geyser.entity.type;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
-import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket;
import org.geysermc.geyser.entity.EntityDefinition;
-import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.TooltipOptions;
import org.geysermc.geyser.session.GeyserSession;
@@ -72,20 +70,22 @@ public class FireworkEntity extends Entity {
// and checks to make sure the player that is gliding is the one getting sent the packet
// or else every player near the gliding player will boost too.
if (optional.isPresent() && optional.getAsInt() == session.getPlayerEntity().getEntityId()) {
- PlayerEntity entity = session.getPlayerEntity();
- float yaw = entity.getYaw();
- float pitch = entity.getPitch();
- // Uses math from NukkitX
- entity.setMotion(Vector3f.from(
- -Math.sin(Math.toRadians(yaw)) * Math.cos(Math.toRadians(pitch)) * 2,
- -Math.sin(Math.toRadians(pitch)) * 2,
- Math.cos(Math.toRadians(yaw)) * Math.cos(Math.toRadians(pitch)) * 2));
- // Need to update the EntityMotionPacket or else the player won't boost
- SetEntityMotionPacket entityMotionPacket = new SetEntityMotionPacket();
- entityMotionPacket.setRuntimeEntityId(entity.getGeyserId());
- entityMotionPacket.setMotion(entity.getMotion());
-
- session.sendUpstreamPacket(entityMotionPacket);
+ // TODO Firework rocket boosting is client side. Sending this boost is no longer needed
+ // Good luck to whoever is going to try implementing cancelling firework rocket boosting :)
+// PlayerEntity entity = session.getPlayerEntity();
+// float yaw = entity.getYaw();
+// float pitch = entity.getPitch();
+// // Uses math from NukkitX
+// entity.setMotion(Vector3f.from(
+// -Math.sin(Math.toRadians(yaw)) * Math.cos(Math.toRadians(pitch)) * 2,
+// -Math.sin(Math.toRadians(pitch)) * 2,
+// Math.cos(Math.toRadians(yaw)) * Math.cos(Math.toRadians(pitch)) * 2));
+// // Need to update the EntityMotionPacket or else the player won't boost
+// SetEntityMotionPacket entityMotionPacket = new SetEntityMotionPacket();
+// entityMotionPacket.setRuntimeEntityId(entity.getGeyserId());
+// entityMotionPacket.setMotion(entity.getMotion());
+//
+// session.sendUpstreamPacket(entityMotionPacket);
}
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
index 928e9b764..f4a788518 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
@@ -206,12 +206,16 @@ public class LivingEntity extends Entity {
setFlag(EntityFlag.BLOCKING, isUsingItem && isUsingShield);
// Riptide spin attack
- setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, (xd & 0x04) == 0x04);
+ setSpinAttack((xd & 0x04) == 0x04);
// OptionalPack usage
setFlag(EntityFlag.EMERGING, isUsingItem && isUsingOffhand);
}
+ protected void setSpinAttack(boolean value) {
+ setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, value);
+ }
+
public void setHealth(FloatEntityMetadata entityMetadata) {
this.health = entityMetadata.getPrimitiveValue();
@@ -279,12 +283,12 @@ public class LivingEntity extends Entity {
}
@Override
- protected void setDimensions(Pose pose) {
+ protected void setDimensionsFromPose(Pose pose) {
if (pose == Pose.SLEEPING) {
setBoundingBoxWidth(0.2f);
setBoundingBoxHeight(0.2f);
} else {
- super.setDimensions(pose);
+ super.setDimensionsFromPose(pose);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java
index b5e4ad117..b954bb7a5 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java
@@ -63,12 +63,12 @@ public class GoatEntity extends AnimalEntity {
}
@Override
- protected void setDimensions(Pose pose) {
+ protected void setDimensionsFromPose(Pose pose) {
if (pose == Pose.LONG_JUMPING) {
setBoundingBoxWidth(LONG_JUMPING_WIDTH);
setBoundingBoxHeight(LONG_JUMPING_HEIGHT);
} else {
- super.setDimensions(pose);
+ super.setDimensionsFromPose(pose);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java
index 203a48f19..2def02869 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java
@@ -64,12 +64,12 @@ public class SnifferEntity extends AnimalEntity implements Tickable {
}
@Override
- protected void setDimensions(Pose pose) {
+ protected void setDimensionsFromPose(Pose pose) {
if (getFlag(EntityFlag.DIGGING)) {
setBoundingBoxHeight(DIGGING_HEIGHT);
setBoundingBoxWidth(definition.width());
} else {
- super.setDimensions(pose);
+ super.setDimensionsFromPose(pose);
}
}
@@ -90,7 +90,7 @@ public class SnifferEntity extends AnimalEntity implements Tickable {
setFlag(EntityFlag.DIGGING, snifferState == SnifferState.DIGGING);
setFlag(EntityFlag.RISING, snifferState == SnifferState.RISING);
- setDimensions(pose);
+ setDimensionsFromPose(pose);
if (getFlag(EntityFlag.DIGGING)) {
digTicks = DIG_END;
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java
index ca39bd1e6..6239122f4 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java
@@ -113,12 +113,12 @@ public class CamelEntity extends AbstractHorseEntity implements ClientVehicle {
}
@Override
- protected void setDimensions(Pose pose) {
+ protected void setDimensionsFromPose(Pose pose) {
if (pose == Pose.SITTING) {
setBoundingBoxHeight(definition.height() - SITTING_HEIGHT_DIFFERENCE);
setBoundingBoxWidth(definition.width());
} else {
- super.setDimensions(pose);
+ super.setDimensionsFromPose(pose);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
index 2bdbb56df..dd48ca739 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
@@ -416,7 +416,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
}
@Override
- protected void setDimensions(Pose pose) {
+ public void setDimensionsFromPose(Pose pose) {
float height;
float width;
switch (pose) {
@@ -433,7 +433,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
width = 0.2f;
}
default -> {
- super.setDimensions(pose);
+ super.setDimensionsFromPose(pose);
return;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
index e0422036f..cf973a07d 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
@@ -31,6 +31,7 @@ import lombok.Setter;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
@@ -38,13 +39,20 @@ import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
+import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.level.block.Blocks;
+import org.geysermc.geyser.level.block.property.Properties;
+import org.geysermc.geyser.level.block.type.BlockState;
+import org.geysermc.geyser.level.block.type.TrapDoorBlock;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.session.cache.tags.BlockTag;
import org.geysermc.geyser.util.AttributeUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.geyser.util.MathUtils;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.GlobalPos;
@@ -52,6 +60,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable;
import java.util.Collections;
import java.util.List;
@@ -196,6 +206,16 @@ public class SessionPlayerEntity extends PlayerEntity {
}
}
+ @Override
+ protected void setGliding(boolean value) {
+ session.setGliding(value);
+ }
+
+ @Override
+ protected void setSpinAttack(boolean value) {
+ session.setSpinAttack(value);
+ }
+
/**
* Since 1.19.40, the client must be re-informed of its bounding box on respawn
* See issue 3370
@@ -428,4 +448,67 @@ public class SessionPlayerEntity extends PlayerEntity {
return velocity + 0.1F * session.getEffectCache().getJumpPower();
}
+
+ public boolean isOnClimbableBlock() {
+ if (session.getGameMode() == GameMode.SPECTATOR) {
+ return false;
+ }
+ Vector3i pos = getPosition().down(EntityDefinitions.PLAYER.offset()).toInt();
+ BlockState state = session.getGeyser().getWorldManager().blockAt(session, pos);
+ if (session.getTagCache().is(BlockTag.CLIMBABLE, state.block())) {
+ return true;
+ }
+
+ if (state.block() instanceof TrapDoorBlock) {
+ if (!state.getValue(Properties.OPEN)) {
+ return false;
+ } else {
+ BlockState belowState = session.getGeyser().getWorldManager().blockAt(session, pos.down());
+ return belowState.is(Blocks.LADDER) && belowState.getValue(Properties.HORIZONTAL_FACING) == state.getValue(Properties.HORIZONTAL_FACING);
+ }
+ }
+ return false;
+ }
+
+ public boolean canStartGliding() {
+ // You can't start gliding when levitation is applied
+ if (session.getEffectCache().getEntityEffects().contains(Effect.LEVITATION)) {
+ return false;
+ }
+
+ if (this.isOnClimbableBlock() || session.getPlayerEntity().isOnGround()) {
+ return false;
+ }
+
+ if (session.getCollisionManager().isPlayerTouchingWater()) {
+ return false;
+ }
+
+ // Unfortunately gliding is still client-side, so we cannot force the client to glide even
+ // if we wanted to. However, we still need to check that gliding is possible even with, say,
+ // an elytra that does not have the glider component.
+ for (Map.Entry entry : session.getPlayerInventory().getEquipment().entrySet()) {
+ if (entry.getValue().getComponent(DataComponentTypes.GLIDER) != null) {
+ Equippable equippable = entry.getValue().getComponent(DataComponentTypes.EQUIPPABLE);
+ if (equippable != null && equippable.slot() == entry.getKey() && !entry.getValue().nextDamageWillBreak()) {
+ return true;
+ }
+ }
+
+ // Bedrock will NOT allow flight when not wearing an elytra; even if it doesn't have a glider component
+ if (entry.getKey() == EquipmentSlot.CHESTPLATE && !entry.getValue().asItem().equals(Items.ELYTRA)) {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ public void forceFlagUpdate() {
+ setFlagsDirty(true);
+ }
+
+ public boolean isGliding() {
+ return getFlag(EntityFlag.GLIDING);
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
index a66b07598..5f4ce6b45 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
@@ -126,14 +126,14 @@ public class GeyserItemStack {
}
/**
- * @return the {@link DataComponents} that aren't the base/default components.
+ * @return the {@link DataComponents} patch that's sent over the network.
*/
public @Nullable DataComponents getComponents() {
return isEmpty() ? null : components;
}
/**
- * @return whether this GeyserItemStack has any additional components on top of
+ * @return whether this GeyserItemStack has any component modifications additional to
* the base item components.
*/
public boolean hasNonBaseComponents() {
@@ -159,16 +159,13 @@ public class GeyserItemStack {
*/
@Nullable
public T getComponent(@NonNull DataComponentType type) {
- if (components == null) {
- return asItem().getComponent(type);
+ // A data component patch may contain null values to remove base components
+ // e.g. an elytra without the glider component
+ if (components != null && components.contains(type)) {
+ return components.get(type);
}
- T value = components.get(type);
- if (value == null) {
- return asItem().getComponent(type);
- }
-
- return value;
+ return asItem().getComponent(type);
}
public T getComponentElseGet(@NonNull DataComponentType type, Supplier supplier) {
@@ -251,6 +248,24 @@ public class GeyserItemStack {
return new ItemStackSlotDisplay(this.getItemStack());
}
+ public int getMaxDamage() {
+ return getComponentElseGet(DataComponentTypes.MAX_DAMAGE, () -> 0);
+ }
+
+ public int getDamage() {
+ // Damage can't be negative
+ int damage = Math.max(this.getComponentElseGet(DataComponentTypes.DAMAGE, () -> 0), 0);
+ return Math.min(damage, this.getMaxDamage());
+ }
+
+ public boolean nextDamageWillBreak() {
+ return this.isDamageable() && this.getDamage() >= this.getMaxDamage() - 1;
+ }
+
+ public boolean isDamageable() {
+ return getComponent(DataComponentTypes.MAX_DAMAGE) != null && getComponent(DataComponentTypes.UNBREAKABLE) == null && getComponent(DataComponentTypes.DAMAGE) != null;
+ }
+
public Item asItem() {
if (isEmpty()) {
return Items.AIR;
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
index a3af293d9..afc9a57b2 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
@@ -31,9 +31,12 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.jetbrains.annotations.Range;
+import java.util.Map;
+
@Getter
public class PlayerInventory extends Inventory {
/**
@@ -83,6 +86,18 @@ public class PlayerInventory extends Inventory {
return items[36 + heldItemSlot];
}
+ // TODO other equipment slots
+ public Map getEquipment() {
+ return Map.of(
+ EquipmentSlot.MAIN_HAND, getItemInHand(),
+ EquipmentSlot.OFF_HAND, items[45],
+ EquipmentSlot.BOOTS, items[8],
+ EquipmentSlot.LEGGINGS, items[7],
+ EquipmentSlot.CHESTPLATE, items[6],
+ EquipmentSlot.HELMET, items[5]
+ );
+ }
+
public boolean eitherHandMatchesItem(@NonNull Item item) {
return getItemInHand().asItem() == item || getItemInHand(Hand.OFF_HAND).asItem() == item;
}
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 ff6557935..09e23e27a 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
@@ -424,6 +424,14 @@ public class CollisionManager {
return state.is(Blocks.WATER) && state.getValue(Properties.LEVEL) == 0;
}
+ /**
+ * @return if the player is currently touching water
+ */
+ public boolean isPlayerTouchingWater() {
+ BlockState state = session.getGeyser().getWorldManager().blockAt(session, session.getPlayerEntity().position().toInt());
+ return state.is(Blocks.WATER);
+ }
+
public boolean isWaterInEyes() {
double eyeX = playerBoundingBox.getMiddleX();
double eyeY = playerBoundingBox.getMiddleY() - playerBoundingBox.getSizeY() / 2d + session.getEyeHeight();
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 01e04fa9a..40aff17f9 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -1260,6 +1260,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
setSneaking(false);
}
+ public void setSpinAttack(boolean spinAttack) {
+ switchPose(spinAttack, EntityFlag.DAMAGE_NEARBY_MOBS, Pose.SPIN_ATTACK);
+ }
+
+ public void setGliding(boolean gliding) {
+ switchPose(gliding, EntityFlag.GLIDING, Pose.FALL_FLYING);
+ }
+
private void setSneaking(boolean sneaking) {
this.sneaking = sneaking;
@@ -1296,22 +1304,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
playerEntity.updateBedrockMetadata();
return;
}
- toggleSwimmingPose(swimming, EntityFlag.SWIMMING);
+ switchPose(swimming, EntityFlag.SWIMMING, Pose.SWIMMING);
}
public void setCrawling(boolean crawling) {
- toggleSwimmingPose(crawling, EntityFlag.CRAWLING);
+ switchPose(crawling, EntityFlag.CRAWLING, Pose.SWIMMING);
}
- private void toggleSwimmingPose(boolean crawling, EntityFlag flag) {
- if (crawling) {
- this.pose = Pose.SWIMMING;
- playerEntity.setBoundingBoxHeight(0.6f);
- } else {
- this.pose = Pose.STANDING;
- playerEntity.setBoundingBoxHeight(playerEntity.getDefinition().height());
- }
- playerEntity.setFlag(flag, crawling);
+ private void switchPose(boolean value, EntityFlag flag, Pose pose) {
+ this.pose = value ? pose : Pose.STANDING;
+ playerEntity.setDimensionsFromPose(this.pose);
+ playerEntity.setFlag(flag, value);
playerEntity.updateBedrockMetadata();
}
@@ -1975,7 +1978,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
FALL_FLYING, // Elytra
SPIN_ATTACK -> 0.4f; // Trident spin attack
case SLEEPING -> 0.2f;
- default -> EntityDefinitions.PLAYER.offset();
+ default -> EntityDefinitions.PLAYER.offset(); // 1.62F
};
}
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 b6b0703d1..8ab120ba5 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,7 +32,7 @@ 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.entity.type.player.SessionPlayerEntity;
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;
@@ -43,6 +43,7 @@ import java.util.Set;
public final class InputCache {
private final GeyserSession session;
private ServerboundPlayerInputPacket inputPacket = new ServerboundPlayerInputPacket(false, false, false, false, false, false, false);
+ @Setter
private boolean lastHorizontalCollision;
private int ticksSinceLastMovePacket;
@Getter @Setter
@@ -56,7 +57,7 @@ public final class InputCache {
this.session = session;
}
- public void processInputs(PlayerEntity entity, PlayerAuthInputPacket packet) {
+ public void processInputs(SessionPlayerEntity 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;
@@ -77,19 +78,25 @@ 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)
.withBackward(down)
.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(sneaking)
- .withSprint(bedrockInput.contains(PlayerAuthInputData.SPRINTING)); // SPRINTING will trigger even if the player isn't moving
+ // https://mojang.github.io/bedrock-protocol-docs/html/enums.html
+ // using the "raw" values allows us sending key presses even with locked input
+ .withJump(bedrockInput.contains(PlayerAuthInputData.JUMP_CURRENT_RAW))
+ .withShift(bedrockInput.contains(PlayerAuthInputData.SNEAK_CURRENT_RAW))
+ .withSprint(bedrockInput.contains(PlayerAuthInputData.SPRINT_DOWN));
// Send sneaking state before inputs, matches Java client
+ boolean sneaking = bedrockInput.contains(PlayerAuthInputData.SNEAKING) ||
+ // DESCEND_BLOCK is ONLY sent while mobile clients are descending scaffolding.
+ // PERSIST_SNEAK is ALWAYS sent by mobile clients.
+ // While we could use SNEAK_CURRENT_RAW, that would also be sent with locked inputs.
+ // fixes https://github.com/GeyserMC/Geyser/issues/5384
+ (bedrockInput.contains(PlayerAuthInputData.DESCEND_BLOCK) && bedrockInput.contains(PlayerAuthInputData.PERSIST_SNEAK));
if (oldInputPacket.isShift() != sneaking) {
if (sneaking) {
session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.START_SNEAKING));
@@ -121,8 +128,4 @@ public final class InputCache {
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/BedrockRequestAbilityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRequestAbilityTranslator.java
deleted file mode 100644
index d0c29c6a9..000000000
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRequestAbilityTranslator.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (c) 2019-2022 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;
-
-import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
-import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
-import org.cloudburstmc.protocol.bedrock.data.Ability;
-import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
-import org.cloudburstmc.protocol.bedrock.packet.RequestAbilityPacket;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.translator.protocol.PacketTranslator;
-import org.geysermc.geyser.translator.protocol.Translator;
-
-/**
- * Replaces the AdventureSettingsPacket completely in 1.19.30.
- */
-@Translator(packet = RequestAbilityPacket.class)
-public class BedrockRequestAbilityTranslator extends PacketTranslator {
-
- @Override
- public void translate(GeyserSession session, RequestAbilityPacket packet) {
- // TODO: Since 1.20.30, this was replaced by a START_FLYING and STOP_FLYING case in BedrockActionTranslator
- if (packet.getAbility() == Ability.FLYING) {
- boolean isFlying = packet.isBoolValue();
- if (!isFlying && session.getGameMode() == GameMode.SPECTATOR) {
- // We should always be flying in spectator mode
- session.sendAdventureSettings();
- return;
- } else if (isFlying && 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();
- return;
- }
-
- session.setFlying(isFlying);
- ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(isFlying);
- session.sendDownstreamGamePacket(abilitiesPacket);
- }
- }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java
index 0d8386bc1..3a2f36d5f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java
@@ -33,7 +33,6 @@ import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.level.physics.CollisionResult;
import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.session.cache.tags.BlockTag;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.mcprotocollib.network.packet.Packet;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket;
@@ -93,7 +92,7 @@ final class BedrockMovePlayer {
}
// Due to how ladder works on Bedrock, we won't get climbing velocity from tick end unless if you're colliding horizontally. So we account for it ourselves.
- boolean onClimbableBlock = session.getTagCache().is(BlockTag.CLIMBABLE, session.getGeyser().getWorldManager().blockAt(session, entity.getPosition().sub(0, EntityDefinitions.PLAYER.offset(), 0).toInt()).block());
+ boolean onClimbableBlock = entity.isOnClimbableBlock();
if (onClimbableBlock && packet.getInputData().contains(PlayerAuthInputData.JUMPING)) {
entity.setLastTickEndVelocity(Vector3f.from(entity.getLastTickEndVelocity().getX(), 0.2F, entity.getLastTickEndVelocity().getZ()));
}
@@ -102,7 +101,8 @@ final class BedrockMovePlayer {
boolean isOnGround;
if (hasVehicle) {
// VERTICAL_COLLISION is not accurate while in a vehicle (as of 1.21.62)
- isOnGround = Math.abs(entity.getLastTickEndVelocity().getY()) < 0.1;
+ // If the player is riding a vehicle or is in spectator mode, onGround is always set to false for the player
+ isOnGround = false;
} else {
isOnGround = packet.getInputData().contains(PlayerAuthInputData.VERTICAL_COLLISION) && entity.getLastTickEndVelocity().getY() < 0;
}
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 7c374c73f..94aadad95 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
@@ -28,17 +28,15 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player.input;
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.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.BoatEntity;
import org.geysermc.geyser.entity.type.Entity;
@@ -48,6 +46,7 @@ import org.geysermc.geyser.entity.type.living.animal.horse.LlamaEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.level.block.type.Block;
+import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@@ -66,6 +65,7 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.Serv
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket;
+import java.util.HashSet;
import java.util.Set;
@Translator(packet = PlayerAuthInputPacket.class)
@@ -78,33 +78,44 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator inputData = packet.getInputData();
+ // These inputs are sent in order, so if e.g. START_GLIDING and STOP_GLIDING are both present,
+ // it's important to make sure we send the last known status instead of both to the Java server.
+ Set leftOverInputData = new HashSet<>(packet.getInputData());
for (PlayerAuthInputData input : inputData) {
+ leftOverInputData.remove(input);
switch (input) {
case PERFORM_ITEM_INTERACTION -> processItemUseTransaction(session, packet.getItemUseTransaction());
case PERFORM_BLOCK_ACTIONS -> BedrockBlockActions.translate(session, packet.getPlayerActions());
- 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_CRAWLING -> session.setCrawling(true);
case STOP_CRAWLING -> session.setCrawling(false);
+ case START_SPRINTING -> {
+ if (!leftOverInputData.contains(PlayerAuthInputData.STOP_SPRINTING)) {
+ // Check if the player is standing on but not surrounded by water; don't allow sprinting in that case
+ // resolves
+ if (!GameProtocol.is1_21_80orHigher(session) && session.getCollisionManager().isPlayerTouchingWater() && !session.getCollisionManager().isPlayerInWater()) {
+ // Update movement speed attribute to prevent sprinting on water. This is fixed in 1.21.80+ natively.
+ UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
+ attributesPacket.setRuntimeEntityId(entity.getGeyserId());
+ attributesPacket.getAttributes().addAll(entity.getAttributes().values());
+ session.sendUpstreamPacket(attributesPacket);
+ } else {
+ sprintPacket = new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.START_SPRINTING);
+ session.setSprinting(true);
+ }
+ }
+ }
+ case STOP_SPRINTING -> {
+ // Don't send sprinting update when we weren't sprinting
+ if (!leftOverInputData.contains(PlayerAuthInputData.START_SPRINTING) && session.isSprinting()) {
+ sprintPacket = new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.STOP_SPRINTING);
+ session.setSprinting(false);
+ }
+ }
case START_FLYING -> { // Since 1.20.30
if (session.isCanFly()) {
if (session.getGameMode() == GameMode.SPECTATOR) {
@@ -123,16 +134,9 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator {
@@ -140,12 +144,37 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator {
- // Otherwise gliding will not work in creative
- ServerboundPlayerAbilitiesPacket playerAbilitiesPacket = new ServerboundPlayerAbilitiesPacket(false);
- session.sendDownstreamGamePacket(playerAbilitiesPacket);
- sendPlayerGlideToggle(session, entity);
+ // Bedrock can send both start_glide and stop_glide in the same packet.
+ // We only want to start gliding if the client has not stopped gliding in the same tick.
+ // last replicated on 1.21.70 by "walking" and jumping while in water
+ if (!leftOverInputData.contains(PlayerAuthInputData.STOP_GLIDING)) {
+ if (entity.canStartGliding()) {
+ // On Java you can't start gliding while flying
+ if (session.isFlying()) {
+ session.setFlying(false);
+ session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(false));
+ }
+ session.setGliding(true);
+ session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_ELYTRA_FLYING));
+ } else {
+ entity.forceFlagUpdate();
+ session.setGliding(false);
+ // return to flying if we can't start gliding
+ if (session.isFlying()) {
+ session.sendAdventureSettings();
+ }
+ }
+ }
+ }
+ case START_SPIN_ATTACK -> session.setSpinAttack(true);
+ case STOP_SPIN_ATTACK -> session.setSpinAttack(false);
+ case STOP_GLIDING -> {
+ // Java doesn't allow elytra gliding to stop mid-air.
+ boolean shouldBeGliding = entity.isGliding() && entity.canStartGliding();
+ // Always update; Bedrock can get real weird if the gliding state is mismatching
+ entity.forceFlagUpdate();
+ session.setGliding(shouldBeGliding);
}
- case STOP_GLIDING -> sendPlayerGlideToggle(session, entity);
case MISSED_SWING -> {
session.setLastAirHitTick(session.getTicks());
@@ -168,6 +197,16 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator