diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java
index d9a64ccc6..47ae6777a 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java
@@ -41,7 +41,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import java.util.UUID;
-public class BoatEntity extends Entity implements Tickable {
+public class BoatEntity extends Entity implements Leashable, Tickable {
/**
* Required when IS_BUOYANT is sent in order for boats to work in the water.
@@ -65,6 +65,8 @@ public class BoatEntity extends Entity implements Tickable {
@Getter
private int variant;
+ private long leashHolderBedrockId = -1;
+
// Looks too fast and too choppy with 0.1f, which is how I believe the Microsoftian client handles it
private final float ROWING_SPEED = 0.1f;
@@ -147,8 +149,18 @@ public class BoatEntity extends Entity implements Tickable {
}
}
+ @Override
+ public void setLeashHolderBedrockId(long bedrockId) {
+ this.leashHolderBedrockId = bedrockId;
+ dirtyMetadata.put(EntityDataTypes.LEASH_HOLDER, bedrockId);
+ }
+
@Override
protected InteractiveTag testInteraction(Hand hand) {
+ InteractiveTag tag = super.testInteraction(hand);
+ if (tag != InteractiveTag.NONE) {
+ return tag;
+ }
if (session.isSneaking()) {
return InteractiveTag.NONE;
} else if (passengers.size() < 2) {
@@ -160,6 +172,10 @@ public class BoatEntity extends Entity implements Tickable {
@Override
public InteractionResult interact(Hand hand) {
+ InteractionResult result = super.interact(hand);
+ if (result != InteractionResult.PASS) {
+ return result;
+ }
if (session.isSneaking()) {
return InteractionResult.PASS;
} else {
@@ -191,6 +207,11 @@ public class BoatEntity extends Entity implements Tickable {
}
}
+ @Override
+ public long leashHolderBedrockId() {
+ return leashHolderBedrockId;
+ }
+
private void sendAnimationPacket(GeyserSession session, Entity rower, AnimatePacket.Action action, float rowTime) {
AnimatePacket packet = new AnimatePacket();
packet.setRuntimeEntityId(rower.getGeyserId());
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 6267ee791..08e87dc03 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
@@ -40,6 +40,7 @@ import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.GeyserDirtyMetadata;
import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager;
+import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.EntityUtils;
@@ -557,6 +558,17 @@ public class Entity implements GeyserEntity {
* Should usually mirror {@link #interact(Hand)} without any side effects.
*/
protected InteractiveTag testInteraction(Hand hand) {
+ if (isAlive() && this instanceof Leashable leashable) {
+ if (leashable.leashHolderBedrockId() == session.getPlayerEntity().getGeyserId()) {
+ // Note this might be client side. Has yet to be an issue though, as of Java 1.21.
+ return InteractiveTag.REMOVE_LEASH;
+ }
+ if (session.getPlayerInventory().getItemInHand(hand).asItem() == Items.LEAD && leashable.canBeLeashed()) {
+ // We shall leash
+ return InteractiveTag.LEASH;
+ }
+ }
+
return InteractiveTag.NONE;
}
@@ -565,6 +577,18 @@ public class Entity implements GeyserEntity {
* to ensure packet parity as well as functionality parity (such as sound effect responses).
*/
public InteractionResult interact(Hand hand) {
+ if (isAlive() && this instanceof Leashable leashable) {
+ if (leashable.leashHolderBedrockId() == session.getPlayerEntity().getGeyserId()) {
+ // Note this might also update client side (a theoretical Geyser/client desync and Java parity issue).
+ // Has yet to be an issue though, as of Java 1.21.
+ return InteractionResult.SUCCESS;
+ }
+ if (session.getPlayerInventory().getItemInHand(hand).asItem() == Items.LEAD && leashable.canBeLeashed()) {
+ // We shall leash
+ return InteractionResult.SUCCESS;
+ }
+ }
+
return InteractionResult.PASS;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java b/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java
new file mode 100644
index 000000000..64d95ba3c
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java
@@ -0,0 +1,44 @@
+/*
+ * 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.entity.type;
+
+/**
+ * I can haz lead
+ * (The item, not the mineral)
+ */
+public interface Leashable {
+ void setLeashHolderBedrockId(long bedrockId);
+
+ long leashHolderBedrockId();
+
+ default boolean canBeLeashed() {
+ return isNotLeashed();
+ }
+
+ default boolean isNotLeashed() {
+ return leashHolderBedrockId() == -1L;
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java
index 8f81125d0..f4b80edf1 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java
@@ -38,7 +38,7 @@ public class AmbientEntity extends MobEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return false;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java
index 6182a27f4..a0ea79d67 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java
@@ -43,7 +43,7 @@ public class DolphinEntity extends WaterEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java
index 95145ae60..9accf178f 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java
@@ -25,12 +25,12 @@
package org.geysermc.geyser.entity.type.living;
-import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.type.Leashable;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
@@ -43,11 +43,10 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import java.util.UUID;
-public class MobEntity extends LivingEntity {
+public class MobEntity extends LivingEntity implements Leashable {
/**
* If another mob is holding this mob by a leash, this variable tracks their Bedrock entity ID.
*/
- @Getter
private long leashHolderBedrockId;
public MobEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
@@ -65,6 +64,7 @@ public class MobEntity extends LivingEntity {
setFlag(EntityFlag.NO_AI, (xd & 0x01) == 0x01);
}
+ @Override
public void setLeashHolderBedrockId(long bedrockId) {
this.leashHolderBedrockId = bedrockId;
dirtyMetadata.put(EntityDataTypes.LEASH_HOLDER, bedrockId);
@@ -79,10 +79,7 @@ public class MobEntity extends LivingEntity {
return InteractiveTag.REMOVE_LEASH;
} else {
GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand);
- if (itemStack.asItem() == Items.LEAD && canBeLeashed()) {
- // We shall leash
- return InteractiveTag.LEASH;
- } else if (itemStack.asItem() == Items.NAME_TAG) {
+ if (itemStack.asItem() == Items.NAME_TAG) {
InteractionResult result = checkInteractWithNameTag(itemStack);
if (result.consumesAction()) {
return InteractiveTag.NAME;
@@ -99,9 +96,6 @@ public class MobEntity extends LivingEntity {
if (!isAlive()) {
// dead lol
return InteractionResult.PASS;
- } else if (leashHolderBedrockId == session.getPlayerEntity().getGeyserId()) {
- // TODO looks like the client assumes it will go through and removes the attachment itself?
- return InteractionResult.SUCCESS;
} else {
GeyserItemStack itemInHand = session.getPlayerInventory().getItemInHand(hand);
InteractionResult result = checkPriorityInteractions(itemInHand);
@@ -115,10 +109,7 @@ public class MobEntity extends LivingEntity {
}
private InteractionResult checkPriorityInteractions(GeyserItemStack itemInHand) {
- if (itemInHand.asItem() == Items.LEAD && canBeLeashed()) {
- // We shall leash
- return InteractionResult.SUCCESS;
- } else if (itemInHand.asItem() == Items.NAME_TAG) {
+ if (itemInHand.asItem() == Items.NAME_TAG) {
InteractionResult result = checkInteractWithNameTag(itemInHand);
if (result.consumesAction()) {
return result;
@@ -143,12 +134,14 @@ public class MobEntity extends LivingEntity {
return InteractionResult.PASS;
}
- protected boolean canBeLeashed() {
+ @Override
+ public boolean canBeLeashed() {
return isNotLeashed() && !isEnemy();
}
- protected final boolean isNotLeashed() {
- return leashHolderBedrockId == -1L;
+ @Override
+ public long leashHolderBedrockId() {
+ return leashHolderBedrockId;
}
/**
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java
index 80a5af442..6285bd9a4 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java
@@ -122,7 +122,7 @@ public class SquidEntity extends WaterEntity implements Tickable {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return isNotLeashed();
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java
index a847c4cd7..ae9d0d659 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java
@@ -38,7 +38,7 @@ public class WaterEntity extends CreatureEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return false;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java
index a87b1dd5e..a0ab56ead 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java
@@ -72,7 +72,7 @@ public class AxolotlEntity extends AnimalEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java
index 74c937417..cc23fc607 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java
@@ -63,7 +63,7 @@ public class HoglinEntity extends AnimalEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return isNotLeashed();
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java
index 79401f63f..aaa7c2d7e 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java
@@ -123,7 +123,7 @@ public class PandaEntity extends AnimalEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return false;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java
index b3c1128e3..16901a844 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java
@@ -56,7 +56,7 @@ public class TurtleEntity extends AnimalEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return false;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java
index e16823d37..ea347d193 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java
@@ -84,7 +84,7 @@ public abstract class TameableEntity extends AnimalEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return isNotLeashed();
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java
index 57fb901b4..c6b8051e7 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java
@@ -127,7 +127,7 @@ public class WolfEntity extends TameableEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return !getFlag(EntityFlag.ANGRY) && super.canBeLeashed();
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java
index 64e35e52e..2492aabd7 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java
@@ -47,7 +47,7 @@ public class AbstractMerchantEntity extends AgeableEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return false;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java
index 206746fb9..3d6e381c7 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java
@@ -58,7 +58,7 @@ public class ZoglinEntity extends MonsterEntity {
}
@Override
- protected boolean canBeLeashed() {
+ public boolean canBeLeashed() {
return isNotLeashed();
}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEntityLinkTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEntityLinkTranslator.java
index d595e928f..15d47a285 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEntityLinkTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEntityLinkTranslator.java
@@ -25,15 +25,15 @@
package org.geysermc.geyser.translator.protocol.java.entity;
-import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityLinkPacket;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.geysermc.geyser.entity.type.Entity;
-import org.geysermc.geyser.entity.type.living.MobEntity;
+import org.geysermc.geyser.entity.type.Leashable;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
+import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityLinkPacket;
/**
* Called when a leash is attached, removed or updated from an entity
@@ -44,16 +44,16 @@ public class JavaSetEntityLinkTranslator extends PacketTranslator