diff --git a/src/main/java/net/raphimc/viabedrock/api/model/entity/ArmadilloEntity.java b/src/main/java/net/raphimc/viabedrock/api/model/entity/ArmadilloEntity.java
new file mode 100644
index 000000000..89edc96e7
--- /dev/null
+++ b/src/main/java/net/raphimc/viabedrock/api/model/entity/ArmadilloEntity.java
@@ -0,0 +1,93 @@
+/*
+ * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock
+ * Copyright (C) 2023-2026 RK_01
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package net.raphimc.viabedrock.api.model.entity;
+
+import com.viaversion.viaversion.api.connection.UserConnection;
+import com.viaversion.viaversion.api.minecraft.entities.EntityTypes26_3;
+import com.viaversion.viaversion.api.minecraft.entitydata.EntityData;
+import com.viaversion.viaversion.api.type.types.version.VersionedTypes;
+import net.raphimc.viabedrock.ViaBedrock;
+import net.raphimc.viabedrock.api.util.PacketFactory;
+import net.raphimc.viabedrock.protocol.data.enums.java.EntityEvent;
+import net.raphimc.viabedrock.protocol.data.generated.java.EntityDataFields;
+
+import java.util.List;
+import java.util.UUID;
+
+public class ArmadilloEntity extends MobEntity {
+
+ private static final int IDLE = 0;
+ private static final int ROLLING = 1;
+ private static final int SCARED = 2;
+ private static final int UNROLLING = 3;
+ private static final int ROLL_DURATION_TICKS = 10;
+
+ private String bedrockState;
+ private int javaState = -1;
+ private int rollingTicks;
+ private boolean pendingPeek;
+
+ public ArmadilloEntity(final UserConnection user, final long uniqueId, final long runtimeId, final String type, final int javaId, final UUID javaUuid) {
+ super(user, uniqueId, runtimeId, type, javaId, javaUuid, EntityTypes26_3.ARMADILLO);
+ }
+
+ public void translateState(final String state, final List javaEntityData) {
+ if (state.equals(this.bedrockState)) {
+ return;
+ }
+
+ final int javaState = switch (state) {
+ case "unrolled" -> IDLE;
+ case "rolled_up" -> "unrolled".equals(this.bedrockState) ? ROLLING : SCARED;
+ case "rolled_up_peeking", "rolled_up_relaxing" -> SCARED;
+ case "rolled_up_unrolling" -> UNROLLING;
+ default -> -1;
+ };
+ if (javaState == -1) {
+ ViaBedrock.getPlatform().getLogger().warning("Received unknown armadillo state " + state);
+ return;
+ }
+
+ this.bedrockState = state;
+ this.pendingPeek = state.equals("rolled_up_peeking");
+ this.rollingTicks = javaState == ROLLING ? ROLL_DURATION_TICKS : 0;
+ if (this.javaState != javaState) {
+ this.javaState = javaState;
+ javaEntityData.add(this.stateData());
+ }
+ }
+
+ @Override
+ public void tick() {
+ super.tick();
+ if (this.rollingTicks > 0 && --this.rollingTicks == 0) {
+ this.javaState = SCARED;
+ this.sendEntityData(List.of(this.stateData()));
+ }
+ // The client needs the SCARED metadata before it can play the peek animation.
+ if (this.pendingPeek) {
+ this.pendingPeek = false;
+ PacketFactory.sendJavaEntityEvent(this.user, this, EntityEvent.ARMADILLO_PEEK);
+ }
+ }
+
+ private EntityData stateData() {
+ return new EntityData(this.getJavaEntityDataIndex(EntityDataFields.ARMADILLO_STATE), VersionedTypes.V26_3.entityDataTypes().armadilloState, this.javaState);
+ }
+
+}
diff --git a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java
index 5728257c1..b836f1f7e 100644
--- a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java
+++ b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java
@@ -97,6 +97,10 @@ public void remove() {
public final void updateEntityData(final EntityData[] entityData) {
final List javaEntityData = new ArrayList<>();
this.updateEntityData(entityData, javaEntityData);
+ this.sendEntityData(javaEntityData);
+ }
+
+ public final void sendEntityData(final List javaEntityData) {
final PacketWrapper setEntityData = PacketWrapper.create(ClientboundPackets26_3.SET_ENTITY_DATA, this.user);
setEntityData.write(Types.VAR_INT, this.javaId); // entity id
setEntityData.write(VersionedTypes.V26_3.entityDataList, javaEntityData); // entity data
diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java
index 39c8bf5fa..6ade7aafb 100644
--- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java
+++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java
@@ -35,6 +35,7 @@
import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3;
import com.viaversion.viaversion.util.Key;
import net.raphimc.viabedrock.ViaBedrock;
+import net.raphimc.viabedrock.api.model.entity.ArmadilloEntity;
import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity;
import net.raphimc.viabedrock.api.model.entity.CustomEntity;
import net.raphimc.viabedrock.api.model.entity.Entity;
@@ -94,7 +95,7 @@ public static void register(final BedrockProtocol protocol) {
attributes[i] = new EntityAttribute(name, currentValue, minValue, maxValue);
}
final EntityData[] entityData = wrapper.read(BedrockTypes.ENTITY_DATA_ARRAY); // entity data
- wrapper.read(BedrockTypes.ENTITY_PROPERTIES); // entity properties
+ final EntityProperties entityProperties = wrapper.read(BedrockTypes.ENTITY_PROPERTIES); // entity properties
wrapper.read(BedrockTypes.ENTITY_LINK_ARRAY); // entity links
wrapper.read(BedrockTypes.OPTIONAL_PASSENGER_OF_BLOCKS); // passenger of blocks
@@ -142,7 +143,14 @@ public static void register(final BedrockProtocol protocol) {
if (entity instanceof LivingEntity livingEntity) {
livingEntity.updateAttributes(attributes);
}
- entity.updateEntityData(entityData);
+ if (entity instanceof ArmadilloEntity) {
+ final List javaEntityData = new ArrayList<>();
+ entity.updateEntityData(entityData, javaEntityData);
+ entity.translateEntityProperties(entityProperties, javaEntityData);
+ entity.sendEntityData(javaEntityData);
+ } else {
+ entity.updateEntityData(entityData);
+ }
});
protocol.registerClientbound(ClientboundBedrockPackets.ADD_ITEM_ENTITY, ClientboundPackets26_3.ADD_ENTITY, wrapper -> {
final EntityTracker entityTracker = wrapper.user().get(EntityTracker.class);
diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/EntityMetadataRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/EntityMetadataRewriter.java
index 1f7d7982c..e5392ede6 100644
--- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/EntityMetadataRewriter.java
+++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/EntityMetadataRewriter.java
@@ -27,6 +27,7 @@
import com.viaversion.viaversion.api.type.types.version.VersionedTypes;
import com.viaversion.viaversion.util.Key;
import net.raphimc.viabedrock.ViaBedrock;
+import net.raphimc.viabedrock.api.model.entity.ArmadilloEntity;
import net.raphimc.viabedrock.api.model.entity.Entity;
import net.raphimc.viabedrock.api.model.entity.LivingEntity;
import net.raphimc.viabedrock.api.model.entity.properties.EntityProperty;
@@ -790,6 +791,10 @@ public static void rewriteProperties(final UserConnection user, final Entity ent
ViaBedrock.getPlatform().getLogger().warning("Received int property " + integerProperty.identifier() + " for entity " + entity.type() + " with value " + value);
} else if (property instanceof EntityProperty.EnumProperty enumProperty) {
+ if (i < 0 || i >= enumProperty.possibleValues().length) {
+ ViaBedrock.getPlatform().getLogger().warning("Received invalid enum value " + i + " for property " + enumProperty.identifier() + " on entity " + entity.type());
+ continue;
+ }
final String value = enumProperty.possibleValues()[i];
switch (enumProperty.identifier()) {
@@ -871,6 +876,13 @@ public static void rewriteProperties(final UserConnection user, final Entity ent
}
}
}
+ case "minecraft:armadillo_state" -> {
+ if (entity instanceof ArmadilloEntity armadillo) {
+ armadillo.translateState(value, javaEntityData);
+ } else {
+ ViaBedrock.getPlatform().getLogger().warning("Received armadillo_state for non-ARMADILLO entity " + entity.type());
+ }
+ }
case "minecraft:chest_interaction" -> {
if (entity.javaType().is(EntityTypes26_3.COPPER_GOLEM)) {
final int state = switch (value) {
diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java
index 85d216c7e..428d10ed0 100644
--- a/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java
+++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java
@@ -59,7 +59,9 @@ public EntityTracker(final UserConnection user) {
public Entity addEntity(final long uniqueId, final long runtimeId, final String type, final EntityTypes26_3 javaType) {
final UUID javaUuid = UUID.randomUUID();
- if (javaType.isOrHasParent(EntityTypes26_3.ABSTRACT_HORSE)) {
+ if (javaType.is(EntityTypes26_3.ARMADILLO)) {
+ return this.addEntity(new ArmadilloEntity(this.user(), uniqueId, runtimeId, type, this.getNextJavaEntityId(), javaUuid));
+ } else if (javaType.isOrHasParent(EntityTypes26_3.ABSTRACT_HORSE)) {
return this.addEntity(new AbstractHorseEntity(this.user(), uniqueId, runtimeId, type, this.getNextJavaEntityId(), javaUuid, javaType));
} else if (javaType.isOrHasParent(EntityTypes26_3.MOB)) {
return this.addEntity(new MobEntity(this.user(), uniqueId, runtimeId, type, this.getNextJavaEntityId(), javaUuid, javaType));