diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
index fd2432a4c..a33c51952 100644
--- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
+++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
@@ -44,6 +44,7 @@ import org.geysermc.connector.bootstrap.GeyserBootstrap;
 import org.geysermc.connector.command.CommandManager;
 import org.geysermc.connector.common.AuthType;
 import org.geysermc.connector.configuration.GeyserConfiguration;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.metrics.Metrics;
 import org.geysermc.connector.network.ConnectorServerEventHandler;
 import org.geysermc.connector.network.session.GeyserSession;
@@ -153,6 +154,7 @@ public class GeyserConnector {
         BlockRegistries.init();
         Registries.init();
 
+        EntityDefinitions.init();
         ItemTranslator.init();
         MessageTranslator.init();
         LocaleUtils.init();
diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java
index ea6062063..06d554ece 100644
--- a/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java
+++ b/connector/src/main/java/org/geysermc/connector/command/defaults/OffhandCommand.java
@@ -25,8 +25,8 @@
 
 package org.geysermc.connector.command.defaults;
 
+import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockFace;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
 import org.geysermc.connector.GeyserConnector;
 import org.geysermc.connector.command.CommandSender;
@@ -47,7 +47,7 @@ public class OffhandCommand extends GeyserCommand {
         }
 
         ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, BlockUtils.POSITION_ZERO,
-                BlockFace.DOWN);
+                Direction.DOWN);
         session.sendDownstreamPacket(releaseItemPacket);
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java
index 54bec7257..f51516dc6 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java
@@ -26,37 +26,43 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class AbstractArrowEntity extends Entity {
 
-    public AbstractArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AbstractArrowEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
         // Set the correct texture if using the resource pack
-        metadata.getFlags().setFlag(EntityFlag.BRIBED, entityType == EntityType.SPECTRAL_ARROW);
+        dirtyMetadata.getFlags().setFlag(EntityFlag.BRIBED, definition.entityType() == EntityType.SPECTRAL_ARROW);
 
         setMotion(motion);
     }
 
+    public void setArrowFlags(EntityMetadata<Byte> entityMetadata) {
+        byte data = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+
+        setFlag(EntityFlag.CRITICAL, (data & 0x01) == 0x01);
+    }
+
+    // Ignore the rotation sent by the Java server since the
+    // Java client calculates the rotation from the motion
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8) {
-            byte data = (byte) entityMetadata.getValue();
-
-            metadata.getFlags().setFlag(EntityFlag.CRITICAL, (data & 0x01) == 0x01);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setYaw(float yaw) {
     }
 
     @Override
-    public void setRotation(Vector3f rotation) {
-        // Ignore the rotation sent by the Java server since the
-        // Java client calculates the rotation from the motion
+    public void setPitch(float pitch) {
+    }
+
+    @Override
+    public void setHeadYaw(float headYaw) {
     }
 
     @Override
@@ -64,8 +70,8 @@ public class AbstractArrowEntity extends Entity {
         super.setMotion(motion);
 
         double horizontalSpeed = Math.sqrt(motion.getX() * motion.getX() + motion.getZ() * motion.getZ());
-        float yaw = (float) Math.toDegrees(Math.atan2(motion.getX(), motion.getZ()));
-        float pitch = (float) Math.toDegrees(Math.atan2(motion.getY(), horizontalSpeed));
-        rotation = Vector3f.from(yaw, pitch, yaw);
+        this.yaw = (float) Math.toDegrees(Math.atan2(motion.getX(), motion.getZ()));
+        this.pitch = (float) Math.toDegrees(Math.atan2(motion.getY(), horizontalSpeed));
+        this.headYaw = yaw;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/AreaEffectCloudEntity.java b/connector/src/main/java/org/geysermc/connector/entity/AreaEffectCloudEntity.java
index 48283818e..1c2830232 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/AreaEffectCloudEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/AreaEffectCloudEntity.java
@@ -26,44 +26,47 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.level.particle.Particle;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.utils.EffectUtils;
 
+import java.util.UUID;
+
 public class AreaEffectCloudEntity extends Entity {
 
-    public AreaEffectCloudEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-
-        // Without this the cloud doesn't appear,
-        metadata.put(EntityData.AREA_EFFECT_CLOUD_DURATION, 600);
-
-        // This disabled client side shrink of the cloud
-        metadata.put(EntityData.AREA_EFFECT_CLOUD_RADIUS, 0.0f);
-        metadata.put(EntityData.AREA_EFFECT_CLOUD_CHANGE_RATE, -0.005f);
-        metadata.put(EntityData.AREA_EFFECT_CLOUD_CHANGE_ON_PICKUP, -0.5f);
-
-        metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
+    public AreaEffectCloudEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8) {
-            metadata.put(EntityData.AREA_EFFECT_CLOUD_RADIUS, entityMetadata.getValue());
-            metadata.put(EntityData.BOUNDING_BOX_WIDTH, 2.0f * (float) entityMetadata.getValue());
-        } else if (entityMetadata.getId() == 9) {
-            metadata.put(EntityData.EFFECT_COLOR, entityMetadata.getValue());
-        } else if (entityMetadata.getId() == 11) {
-            Particle particle = (Particle) entityMetadata.getValue();
-            int particleId = EffectUtils.getParticleId(session, particle.getType());
-            if (particleId != -1) {
-                metadata.put(EntityData.AREA_EFFECT_CLOUD_PARTICLE_ID, particleId);
-            }
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        // Without this the cloud doesn't appear,
+        dirtyMetadata.put(EntityData.AREA_EFFECT_CLOUD_DURATION, 600);
+
+        // This disabled client side shrink of the cloud
+        dirtyMetadata.put(EntityData.AREA_EFFECT_CLOUD_RADIUS, 0.0f);
+        dirtyMetadata.put(EntityData.AREA_EFFECT_CLOUD_CHANGE_RATE, -0.005f);
+        dirtyMetadata.put(EntityData.AREA_EFFECT_CLOUD_CHANGE_ON_PICKUP, -0.5f);
+
+        setFlag(EntityFlag.FIRE_IMMUNE, true);
+    }
+
+    public void setRadius(EntityMetadata<Float> entityMetadata) {
+        float value = ((FloatEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.AREA_EFFECT_CLOUD_RADIUS, value);
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, 2.0f * value);
+    }
+
+    public void setParticle(EntityMetadata<Particle> entityMetadata) {
+        Particle particle = entityMetadata.getValue();
+        int particleId = EffectUtils.getParticleId(session, particle.getType());
+        if (particleId != -1) {
+            dirtyMetadata.put(EntityData.AREA_EFFECT_CLOUD_PARTICLE_ID, particleId);
         }
-        super.updateBedrockMetadata(entityMetadata, session);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java b/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java
index 025747940..7069f5cfb 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java
@@ -26,13 +26,16 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.packet.AnimatePacket;
 import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
-import org.geysermc.connector.entity.type.EntityType;
+import lombok.Getter;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 public class BoatEntity extends Entity {
@@ -52,28 +55,36 @@ public class BoatEntity extends Entity {
     private boolean isPaddlingRight;
     private float paddleTimeRight;
 
+    /**
+     * Saved for using the "pick" functionality on a boat.
+     */
+    @Getter
+    private int variant;
+
     // 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.05f;
 
-    public BoatEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position.add(0d, entityType.getOffset(), 0d), motion, rotation.add(90, 0, 90));
+    public BoatEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        // Initial rotation is incorrect
+        super(session, entityId, geyserId, uuid, definition, position.add(0d, definition.offset(), 0d), motion, yaw + 90, 0, yaw + 90);
 
         // Required to be able to move on land 1.16.200+ or apply gravity not in the water 1.16.100+
-        metadata.put(EntityData.IS_BUOYANT, (byte) 1);
-        metadata.put(EntityData.BUOYANCY_DATA, BUOYANCY_DATA);
+        dirtyMetadata.put(EntityData.IS_BUOYANT, (byte) 1);
+        dirtyMetadata.put(EntityData.BUOYANCY_DATA, BUOYANCY_DATA);
     }
 
     @Override
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
         // We don't include the rotation (y) as it causes the boat to appear sideways
-        setPosition(position.add(0d, this.entityType.getOffset(), 0d));
-        setRotation(Vector3f.from(rotation.getX() + 90, 0, rotation.getX() + 90));
+        setPosition(position.add(0d, this.definition.offset(), 0d));
+        this.yaw = yaw + 90;
+        this.headYaw = yaw + 90;
         setOnGround(isOnGround);
 
         MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
         moveEntityPacket.setRuntimeEntityId(geyserId);
         // Minimal glitching when ClientboundMoveVehiclePacket is sent
-        moveEntityPacket.setPosition(session.getRidingVehicleEntity() == this ? position.up(EntityType.PLAYER.getOffset() - this.entityType.getOffset()) : this.position);
+        moveEntityPacket.setPosition(session.getRidingVehicleEntity() == this ? position.up(EntityDefinitions.PLAYER.offset() - this.definition.offset()) : this.position);
         moveEntityPacket.setRotation(getBedrockRotation());
         moveEntityPacket.setOnGround(isOnGround);
         moveEntityPacket.setTeleported(teleported);
@@ -84,91 +95,62 @@ public class BoatEntity extends Entity {
     /**
      * Move the boat without making the adjustments needed to translate from Java
      */
-    public void moveAbsoluteWithoutAdjustments(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
-        super.moveAbsolute(session, position, Vector3f.from(rotation.getX(), 0, rotation.getX()), isOnGround, teleported);
+    public void moveAbsoluteWithoutAdjustments(Vector3f position, float yaw, boolean isOnGround, boolean teleported) {
+        super.moveAbsolute(position, yaw, 0, yaw, isOnGround, teleported);
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
-        super.moveRelative(session, relX, relY, relZ, Vector3f.from(rotation.getX(), 0, rotation.getX()), isOnGround);
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+        super.moveRelative(relX, relY, relZ, yaw, 0, yaw, isOnGround);
     }
 
     @Override
-    public void updatePositionAndRotation(GeyserSession session, double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) {
-        moveRelative(session, moveX, moveY, moveZ, yaw + 90, pitch, isOnGround);
+    public void updatePositionAndRotation(double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) {
+        moveRelative(moveX, moveY, moveZ, yaw + 90, pitch, isOnGround);
     }
 
     @Override
-    public void updateRotation(GeyserSession session, float yaw, float pitch, boolean isOnGround) {
-        moveRelative(session, 0, 0, 0, Vector3f.from(yaw + 90, 0, 0), isOnGround);
+    public void updateRotation(float yaw, float pitch, boolean isOnGround) {
+        moveRelative(0, 0, 0, yaw + 90, 0, 0, isOnGround);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        // Time since last hit
-        if (entityMetadata.getId() == 8) {
-            metadata.put(EntityData.HURT_TIME, entityMetadata.getValue());
-        }
+    public void setVariant(EntityMetadata<Integer> entityMetadata) {
+        variant = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.VARIANT, variant);
+    }
 
-        // Rocking direction
-        if (entityMetadata.getId() == 9) {
-            metadata.put(EntityData.HURT_DIRECTION, entityMetadata.getValue());
-        }
-
-        // 'Health' in Bedrock, damage taken in Java
-        if (entityMetadata.getId() == 10) {
-            // Not exactly health but it makes motion in Bedrock
-            metadata.put(EntityData.HEALTH, 40 - ((int) (float) entityMetadata.getValue()));
-        }
-
-        if (entityMetadata.getId() == 11) {
-            metadata.put(EntityData.VARIANT, entityMetadata.getValue());
-        } else if (entityMetadata.getId() == 12) {
-            isPaddlingLeft = (boolean) entityMetadata.getValue();
-            if (isPaddlingLeft) {
-                // Java sends simply "true" and "false" (is_paddling_left), Bedrock keeps sending packets as you're rowing
-                // This is an asynchronous method that emulates Bedrock rowing until "false" is sent.
-                paddleTimeLeft = 0f;
-                if (!this.passengers.isEmpty()) {
-                    // Get the entity by the first stored passenger and convey motion in this manner
-                    Entity entity = session.getEntityCache().getEntityByJavaId(this.passengers.iterator().nextLong());
-                    if (entity != null) {
-                        updateLeftPaddle(session, entity);
-                    }
+    public void setPaddlingLeft(EntityMetadata<Boolean> entityMetadata) {
+        isPaddlingLeft = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        if (isPaddlingLeft) {
+            // Java sends simply "true" and "false" (is_paddling_left), Bedrock keeps sending packets as you're rowing
+            // This is an asynchronous method that emulates Bedrock rowing until "false" is sent.
+            paddleTimeLeft = 0f;
+            if (!this.passengers.isEmpty()) {
+                // Get the entity by the first stored passenger and convey motion in this manner
+                Entity entity = session.getEntityCache().getEntityByJavaId(this.passengers.iterator().nextLong());
+                if (entity != null) {
+                    updateLeftPaddle(session, entity);
                 }
-            } else {
-                // Indicate that the row position should be reset
-                metadata.put(EntityData.ROW_TIME_LEFT, 0.0f);
             }
+        } else {
+            // Indicate that the row position should be reset
+            dirtyMetadata.put(EntityData.ROW_TIME_LEFT, 0.0f);
         }
-        else if (entityMetadata.getId() == 13) {
-            isPaddlingRight = (boolean) entityMetadata.getValue();
-            if (isPaddlingRight) {
-                paddleTimeRight = 0f;
-                if (!this.passengers.isEmpty()) {
-                    Entity entity = session.getEntityCache().getEntityByJavaId(this.passengers.iterator().nextLong());
-                    if (entity != null) {
-                        updateRightPaddle(session, entity);
-                    }
-                }
-            } else {
-                metadata.put(EntityData.ROW_TIME_RIGHT, 0.0f);
-            }
-        } else if (entityMetadata.getId() == 14) {
-            // Possibly - I don't think this does anything?
-            metadata.put(EntityData.BOAT_BUBBLE_TIME, entityMetadata.getValue());
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
     }
 
-    @Override
-    public void updateBedrockMetadata(GeyserSession session) {
-        super.updateBedrockMetadata(session);
-
-        // As these indicate to reset rowing, remove them until it is time to send them out again.
-        metadata.remove(EntityData.ROW_TIME_LEFT);
-        metadata.remove(EntityData.ROW_TIME_RIGHT);
+    public void setPaddlingRight(EntityMetadata<Boolean> entityMetadata) {
+        isPaddlingRight = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        if (isPaddlingRight) {
+            paddleTimeRight = 0f;
+            if (!this.passengers.isEmpty()) {
+                Entity entity = session.getEntityCache().getEntityByJavaId(this.passengers.iterator().nextLong());
+                if (entity != null) {
+                    updateRightPaddle(session, entity);
+                }
+            }
+        } else {
+            dirtyMetadata.put(EntityData.ROW_TIME_RIGHT, 0.0f);
+        }
     }
 
     private void updateLeftPaddle(GeyserSession session, Entity rower) {
diff --git a/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java
index c04e9f0b7..fcd4e05a3 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java
@@ -25,42 +25,33 @@
 
 package org.geysermc.connector.entity;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import net.kyori.adventure.text.Component;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.connector.network.translators.chat.MessageTranslator;
+
+import java.util.UUID;
 
 public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity {
 
-    public CommandBlockMinecartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-        // Required, or else the GUI will not open
-        metadata.put(EntityData.CONTAINER_TYPE, (byte) 16);
-        metadata.put(EntityData.CONTAINER_BASE_SIZE, 1);
-        // Required, or else the client does not bother to send a packet back with the new information
-        metadata.put(EntityData.COMMAND_BLOCK_ENABLED, (byte) 1);
+    public CommandBlockMinecartEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 14) {
-            metadata.put(EntityData.COMMAND_BLOCK_COMMAND, entityMetadata.getValue());
-        }
-        if (entityMetadata.getId() == 15) {
-            metadata.put(EntityData.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage((Component) entityMetadata.getValue()));
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    protected void initializeMetadata() {
+        // Required, or else the GUI will not open
+        dirtyMetadata.put(EntityData.CONTAINER_TYPE, (byte) 16);
+        dirtyMetadata.put(EntityData.CONTAINER_BASE_SIZE, 1);
+        // Required, or else the client does not bother to send a packet back with the new information
+        dirtyMetadata.put(EntityData.COMMAND_BLOCK_ENABLED, (byte) 1);
     }
 
     /**
      * By default, the command block shown is purple on Bedrock, which does not match Java Edition's orange.
      */
     @Override
-    public void updateDefaultBlockMetadata(GeyserSession session) {
-        metadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getCommandBlockRuntimeId());
-        metadata.put(EntityData.DISPLAY_OFFSET, 6);
+    public void updateDefaultBlockMetadata() {
+        dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getCommandBlockRuntimeId());
+        dirtyMetadata.put(EntityData.DISPLAY_OFFSET, 6);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java
index 5df7ae440..f4991b312 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java
@@ -26,11 +26,14 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 /**
  * This class is used as a base for minecarts with a default block to display like furnaces and spawners
  */
@@ -40,53 +43,48 @@ public class DefaultBlockMinecartEntity extends MinecartEntity {
     public int customBlockOffset = 0;
     public boolean showCustomBlock = false;
 
-    public DefaultBlockMinecartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public DefaultBlockMinecartEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
-        metadata.put(EntityData.CUSTOM_DISPLAY, (byte) 1);
+        dirtyMetadata.put(EntityData.CUSTOM_DISPLAY, (byte) 1);
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        updateDefaultBlockMetadata(session);
-        super.spawnEntity(session);
+    public void spawnEntity() {
+        updateDefaultBlockMetadata();
+        super.spawnEntity();
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
+    public void setCustomBlock(EntityMetadata<Integer> entityMetadata) {
+        customBlock = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
 
-        // Custom block
-        if (entityMetadata.getId() == 11) {
-            customBlock = (int) entityMetadata.getValue();
-
-            if (showCustomBlock) {
-                metadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(customBlock));
-            }
+        if (showCustomBlock) {
+            dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(customBlock));
         }
-
-        // Custom block offset
-        if (entityMetadata.getId() == 12) {
-            customBlockOffset = (int) entityMetadata.getValue();
-
-            if (showCustomBlock) {
-                metadata.put(EntityData.DISPLAY_OFFSET, customBlockOffset);
-            }
-        }
-
-        // If the custom block should be enabled
-        if (entityMetadata.getId() == 13) {
-            if ((boolean) entityMetadata.getValue()) {
-                showCustomBlock = true;
-                metadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(customBlock));
-                metadata.put(EntityData.DISPLAY_OFFSET, customBlockOffset);
-            } else {
-                showCustomBlock = false;
-                updateDefaultBlockMetadata(session);
-            }
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
     }
 
-    public void updateDefaultBlockMetadata(GeyserSession session) { }
+    @Override
+    public void setCustomBlockOffset(EntityMetadata<Integer> entityMetadata) {
+        customBlockOffset = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+
+        if (showCustomBlock) {
+            dirtyMetadata.put(EntityData.DISPLAY_OFFSET, customBlockOffset);
+        }
+    }
+
+    @Override
+    public void setShowCustomBlock(EntityMetadata<Boolean> entityMetadata) {
+        if (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()) {
+            showCustomBlock = true;
+            dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(customBlock));
+            dirtyMetadata.put(EntityData.DISPLAY_OFFSET, customBlockOffset);
+        } else {
+            showCustomBlock = false;
+            updateDefaultBlockMetadata();
+        }
+    }
+
+    public void updateDefaultBlockMetadata() {
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/EnderCrystalEntity.java b/connector/src/main/java/org/geysermc/connector/entity/EnderCrystalEntity.java
index e4b60980f..a39c479f8 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/EnderCrystalEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/EnderCrystalEntity.java
@@ -31,33 +31,31 @@ import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class EnderCrystalEntity extends Entity {
 
-    public EnderCrystalEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-
-        // Bedrock 1.16.100+ - prevents the entity from appearing on fire itself when fire is underneath it
-        metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
+    public EnderCrystalEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        // Bedrock 1.16.100+ - prevents the entity from appearing on fire itself when fire is underneath it
+        dirtyMetadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
+    }
+
+    public void setBlockTarget(EntityMetadata<Position> entityMetadata) {
         // Show beam
         // Usually performed client-side on Bedrock except for Ender Dragon respawn event
-        if (entityMetadata.getId() == 8) {
-            if (entityMetadata.getValue() instanceof Position pos) {
-                metadata.put(EntityData.BLOCK_TARGET, Vector3i.from(pos.getX(), pos.getY(), pos.getZ()));
-            } else {
-                metadata.put(EntityData.BLOCK_TARGET, Vector3i.ZERO);
-            }
+        Position position = entityMetadata.getValue();
+        if (position != null) {
+            dirtyMetadata.put(EntityData.BLOCK_TARGET, Vector3i.from(position.getX(), position.getY(), position.getZ()));
+        } else {
+            dirtyMetadata.put(EntityData.BLOCK_TARGET, Vector3i.ZERO);
         }
-        // There is a base located on the ender crystal
-        if (entityMetadata.getId() == 9) {
-            metadata.getFlags().setFlag(EntityFlag.SHOW_BOTTOM, (boolean) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/Entity.java b/connector/src/main/java/org/geysermc/connector/entity/Entity.java
index a965b35da..bdeacc9dc 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/Entity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/Entity.java
@@ -26,8 +26,10 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;
@@ -38,19 +40,24 @@ import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
 import com.nukkitx.protocol.bedrock.packet.RemoveEntityPacket;
 import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
 import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Setter;
 import net.kyori.adventure.text.Component;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.chat.MessageTranslator;
 import org.geysermc.connector.utils.MathUtils;
 
+import java.util.UUID;
+
 @Getter
 @Setter
 public class Entity {
+    protected final GeyserSession session;
+
     protected long entityId;
     protected final long geyserId;
+    protected UUID uuid;
 
     protected Vector3f position;
     protected Vector3f motion;
@@ -58,85 +65,120 @@ public class Entity {
     /**
      * x = Yaw, y = Pitch, z = HeadYaw
      */
-    protected Vector3f rotation;
+    protected float yaw;
+    protected float pitch;
+    protected float headYaw;
 
     /**
      * Saves if the entity should be on the ground. Otherwise entities like parrots are flapping when rotating
      */
     protected boolean onGround;
 
-    protected EntityType entityType;
+    protected EntityDefinition<?> definition;
 
     protected boolean valid;
 
-    protected LongOpenHashSet passengers = new LongOpenHashSet();
-    protected EntityDataMap metadata = new EntityDataMap();
+    /* Metadata about this specific entity */
+    @Setter(AccessLevel.NONE)
+    protected float boundingBoxHeight;
+    @Setter(AccessLevel.NONE)
+    protected float boundingBoxWidth;
+    /* Metadata end */
+
+    protected LongOpenHashSet passengers = new LongOpenHashSet();
+    /**
+     * A container to store temporary metadata before it's sent to Bedrock.
+     */
+    protected final EntityDataMap dirtyMetadata = new EntityDataMap();
+    /**
+     * The entity flags for the Bedrock entity.
+     * These must always be saved - if flags are updated and the other values aren't present, the Bedrock client will
+     * think they are set to false.
+     */
+    @Getter(AccessLevel.NONE)
+    protected final EntityFlags flags = new EntityFlags();
+    /**
+     * Indicates if flags have been updated and need to be sent to the client.
+     */
+    @Getter(AccessLevel.NONE)
+    @Setter(AccessLevel.PROTECTED) // For players
+    private boolean flagsDirty = false;
+
+    public Entity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        this.session = session;
 
-    public Entity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
         this.entityId = entityId;
         this.geyserId = geyserId;
-        this.entityType = entityType;
+        this.uuid = uuid;
+        this.definition = definition;
         this.motion = motion;
-        this.rotation = rotation;
+        this.yaw = yaw;
+        this.pitch = pitch;
+        this.headYaw = headYaw;
 
         this.valid = false;
 
         setPosition(position);
         setAir(getMaxAir());
 
-        metadata.put(EntityData.SCALE, 1f);
-        metadata.put(EntityData.COLOR, 0);
-        metadata.put(EntityData.MAX_AIR_SUPPLY, getMaxAir());
-        metadata.put(EntityData.LEASH_HOLDER_EID, -1L);
-        metadata.put(EntityData.BOUNDING_BOX_HEIGHT, entityType.getHeight());
-        metadata.put(EntityData.BOUNDING_BOX_WIDTH, entityType.getWidth());
-        EntityFlags flags = new EntityFlags();
-        flags.setFlag(EntityFlag.HAS_GRAVITY, true);
-        flags.setFlag(EntityFlag.HAS_COLLISION, true);
-        flags.setFlag(EntityFlag.CAN_SHOW_NAME, true);
-        flags.setFlag(EntityFlag.CAN_CLIMB, true);
-        metadata.putFlags(flags);
+        initializeMetadata();
     }
 
-    public void spawnEntity(GeyserSession session) {
+    /**
+     * Called on entity spawn. Used to populate the entity metadata and flags with default values.
+     */
+    protected void initializeMetadata() {
+        dirtyMetadata.put(EntityData.SCALE, 1f);
+        dirtyMetadata.put(EntityData.COLOR, 0);
+        dirtyMetadata.put(EntityData.MAX_AIR_SUPPLY, getMaxAir());
+        setDimensions(Pose.STANDING);
+        setFlag(EntityFlag.HAS_GRAVITY, true);
+        setFlag(EntityFlag.HAS_COLLISION, true);
+        setFlag(EntityFlag.CAN_SHOW_NAME, true);
+        setFlag(EntityFlag.CAN_CLIMB, true);
+    }
+
+    public void spawnEntity() {
         AddEntityPacket addEntityPacket = new AddEntityPacket();
-        addEntityPacket.setIdentifier(entityType.getIdentifier());
+        addEntityPacket.setIdentifier(definition.identifier());
         addEntityPacket.setRuntimeEntityId(geyserId);
         addEntityPacket.setUniqueEntityId(geyserId);
         addEntityPacket.setPosition(position);
         addEntityPacket.setMotion(motion);
         addEntityPacket.setRotation(getBedrockRotation());
-        addEntityPacket.setEntityType(entityType.getType());
-        addEntityPacket.getMetadata().putAll(metadata);
+        addEntityPacket.setEntityType(definition.bedrockId());
+        addEntityPacket.getMetadata().putFlags(flags)
+                .putAll(dirtyMetadata);
         addAdditionalSpawnData(addEntityPacket);
 
         valid = true;
         session.sendUpstreamPacket(addEntityPacket);
 
-        session.getConnector().getLogger().debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")");
+        dirtyMetadata.clear();
+        flagsDirty = false;
+
+        session.getConnector().getLogger().debug("Spawned entity " + getClass().getName() + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")");
     }
 
     /**
      * To be overridden in other entity classes, if additional things need to be done to the spawn entity packet.
      */
     public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) {
-
     }
 
     /**
      * Despawns the entity
      *
-     * @param session The GeyserSession
      * @return can be deleted
      */
-    public boolean despawnEntity(GeyserSession session) {
+    public boolean despawnEntity() {
         if (!valid) return true;
 
         for (long passenger : passengers) { // Make sure all passengers on the despawned entity are updated
             Entity entity = session.getEntityCache().getEntityByJavaId(passenger);
             if (entity == null) continue;
-            entity.getMetadata().getOrCreateFlags().setFlag(EntityFlag.RIDING, false);
-            entity.updateBedrockMetadata(session);
+            entity.setFlag(EntityFlag.RIDING, false);
+            entity.updateBedrockMetadata();
         }
 
         RemoveEntityPacket removeEntityPacket = new RemoveEntityPacket();
@@ -147,12 +189,14 @@ public class Entity {
         return true;
     }
 
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, float yaw, float pitch, boolean isOnGround) {
-        moveRelative(session, relX, relY, relZ, Vector3f.from(yaw, pitch, this.rotation.getZ()), isOnGround);
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, boolean isOnGround) {
+        moveRelative(relX, relY, relZ, yaw, pitch, this.headYaw, isOnGround);
     }
 
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
-        setRotation(rotation);
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+        setYaw(yaw);
+        setPitch(pitch);
+        setHeadYaw(headYaw);
         setOnGround(isOnGround);
         this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
 
@@ -166,13 +210,16 @@ public class Entity {
         session.sendUpstreamPacket(moveEntityPacket);
     }
 
-    public void moveAbsolute(GeyserSession session, Vector3f position, float yaw, float pitch, boolean isOnGround, boolean teleported) {
-        moveAbsolute(session, position, Vector3f.from(yaw, pitch, this.rotation.getZ()), isOnGround, teleported);
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, boolean isOnGround, boolean teleported) {
+        moveAbsolute(position, yaw, pitch, this.headYaw, isOnGround, teleported);
     }
 
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
         setPosition(position);
-        setRotation(rotation);
+        // Setters are intentional so it can be overridden in places like AbstractArrowEntity
+        setYaw(yaw);
+        setPitch(pitch);
+        setHeadYaw(headYaw);
         setOnGround(isOnGround);
 
         MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
@@ -187,28 +234,25 @@ public class Entity {
 
     /**
      * Teleports an entity to a new location. Used in JavaTeleportEntityTranslator.
-     * @param session GeyserSession.
      * @param position The new position of the entity.
      * @param yaw The new yaw of the entity.
      * @param pitch The new pitch of the entity.
      * @param isOnGround Whether the entity is currently on the ground.
      */
-    public void teleport(GeyserSession session, Vector3f position, float yaw, float pitch, boolean isOnGround) {
-        moveAbsolute(session, position, yaw, pitch, isOnGround, false);
+    public void teleport(Vector3f position, float yaw, float pitch, boolean isOnGround) {
+        moveAbsolute(position, yaw, pitch, isOnGround, false);
     }
 
     /**
      * Updates an entity's head position. Used in JavaRotateHeadTranslator.
-     * @param session GeyserSession.
      * @param headYaw The new head rotation of the entity.
      */
-    public void updateHeadLookRotation(GeyserSession session, float headYaw) {
-        moveRelative(session, 0, 0, 0, Vector3f.from(headYaw, rotation.getY(), rotation.getZ()), onGround);
+    public void updateHeadLookRotation(float headYaw) {
+        moveRelative(0, 0, 0, headYaw, pitch, this.headYaw, onGround);
     }
 
     /**
      * Updates an entity's position and rotation. Used in JavaMoveEntityPosRotTranslator.
-     * @param session GeyserSession
      * @param moveX The new X offset of the current position.
      * @param moveY The new Y offset of the current position.
      * @param moveZ The new Z offset of the current position.
@@ -216,106 +260,115 @@ public class Entity {
      * @param pitch The new pitch of the entity.
      * @param isOnGround Whether the entity is currently on the ground.
      */
-    public void updatePositionAndRotation(GeyserSession session, double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) {
-        moveRelative(session, moveX, moveY, moveZ, Vector3f.from(rotation.getX(), pitch, yaw), isOnGround);
+    public void updatePositionAndRotation(double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) {
+        moveRelative(moveX, moveY, moveZ, this.yaw, pitch, yaw, isOnGround);
     }
 
     /**
      * Updates an entity's rotation. Used in JavaMoveEntityRotTranslator.
-     * @param session GeyserSession.
      * @param yaw The new yaw of the entity.
      * @param pitch The new pitch of the entity.
      * @param isOnGround Whether the entity is currently on the ground.
      */
-    public void updateRotation(GeyserSession session, float yaw, float pitch, boolean isOnGround) {
-        updatePositionAndRotation(session, 0, 0, 0, yaw, pitch, isOnGround);
+    public void updateRotation(float yaw, float pitch, boolean isOnGround) {
+        updatePositionAndRotation(0, 0, 0, yaw, pitch, isOnGround);
+    }
+
+    public final boolean getFlag(EntityFlag flag) {
+        return flags.getFlag(flag);
     }
 
     /**
-     * Applies the Java metadata to the local Bedrock metadata copy
-     * @param entityMetadata the Java entity metadata
-     * @param session GeyserSession
+     * Updates a flag value and determines if the flags would need synced with the Bedrock client.
      */
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        switch (entityMetadata.getId()) {
-            case 0:
-                if (entityMetadata.getType() == MetadataType.BYTE) {
-                    byte xd = (byte) entityMetadata.getValue();
-                    metadata.getFlags().setFlag(EntityFlag.ON_FIRE, ((xd & 0x01) == 0x01) && !metadata.getFlags().getFlag(EntityFlag.FIRE_IMMUNE)); // Otherwise immune entities sometimes flicker onfire
-                    metadata.getFlags().setFlag(EntityFlag.SNEAKING, (xd & 0x02) == 0x02);
-                    metadata.getFlags().setFlag(EntityFlag.SPRINTING, (xd & 0x08) == 0x08);
-                    // Swimming is ignored here and instead we rely on the pose
-                    metadata.getFlags().setFlag(EntityFlag.GLIDING, (xd & 0x80) == 0x80);
-
-                    setInvisible(session, (xd & 0x20) == 0x20);
-                }
-                break;
-            case 1: // Air/bubbles
-                setAir((int) entityMetadata.getValue());
-                break;
-            case 2: // custom name
-                setDisplayName(session, (Component) entityMetadata.getValue());
-                break;
-            case 3: // is custom name visible
-                setDisplayNameVisible(entityMetadata);
-                break;
-            case 4: // silent
-                metadata.getFlags().setFlag(EntityFlag.SILENT, (boolean) entityMetadata.getValue());
-                break;
-            case 5: // no gravity
-                metadata.getFlags().setFlag(EntityFlag.HAS_GRAVITY, !(boolean) entityMetadata.getValue());
-                break;
-            case 6: // Pose change - typically used for bounding box and not animation
-                Pose pose = (Pose) entityMetadata.getValue();
-
-                metadata.getFlags().setFlag(EntityFlag.SLEEPING, pose.equals(Pose.SLEEPING));
-                // Triggered when crawling
-                metadata.getFlags().setFlag(EntityFlag.SWIMMING, pose.equals(Pose.SWIMMING));
-                setDimensions(pose);
-                break;
-            case 7: // Freezing ticks
-                // The value that Java edition gives us is in ticks, but Bedrock uses a float percentage of the strength 0.0 -> 1.0
-                // The Java client caps its freezing tick percentage at 140
-                int freezingTicks = Math.min((int) entityMetadata.getValue(), 140);
-                setFreezing(session, freezingTicks / 140f);
-                break;
-        }
+    public final void setFlag(EntityFlag flag, boolean value) {
+        flagsDirty |= flags.setFlag(flag, value);
     }
 
     /**
      * Sends the Bedrock metadata to the client
-     * @param session GeyserSession
      */
-    public void updateBedrockMetadata(GeyserSession session) {
-        if (!valid) return;
+    public void updateBedrockMetadata() {
+        if (!valid) {
+            return;
+        }
 
-        SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
-        entityDataPacket.setRuntimeEntityId(geyserId);
-        entityDataPacket.getMetadata().putAll(metadata);
-        session.sendUpstreamPacket(entityDataPacket);
-    }
+        if (!dirtyMetadata.isEmpty() || flagsDirty) {
+            SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
+            entityDataPacket.setRuntimeEntityId(geyserId);
+            entityDataPacket.getMetadata().putFlags(flags);
+            entityDataPacket.getMetadata().putAll(dirtyMetadata);
+            session.sendUpstreamPacket(entityDataPacket);
 
-    /**
-     * If true, the entity should be shaking on the client's end.
-     *
-     * @return whether {@link EntityFlag#SHAKING} should be set to true.
-     */
-    protected boolean isShaking(GeyserSession session) {
-        return false;
-    }
-
-    protected void setDisplayName(GeyserSession session, Component name) {
-        if (name != null) {
-            String displayName = MessageTranslator.convertMessage(name, session.getLocale());
-            metadata.put(EntityData.NAMETAG, displayName);
-        } else if (!metadata.getString(EntityData.NAMETAG).isEmpty()) {
-            // Clear nametag
-            metadata.put(EntityData.NAMETAG, "");
+            dirtyMetadata.clear();
+            flagsDirty = false;
         }
     }
 
-    protected void setDisplayNameVisible(EntityMetadata entityMetadata) {
-        metadata.put(EntityData.NAMETAG_ALWAYS_SHOW, (byte) ((boolean) entityMetadata.getValue() ? 1 : 0));
+    public void setFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.ON_FIRE, ((xd & 0x01) == 0x01) && !getFlag(EntityFlag.FIRE_IMMUNE)); // Otherwise immune entities sometimes flicker onfire
+        setFlag(EntityFlag.SNEAKING, (xd & 0x02) == 0x02);
+        setFlag(EntityFlag.SPRINTING, (xd & 0x08) == 0x08);
+        // Swimming is ignored here and instead we rely on the pose
+        setFlag(EntityFlag.GLIDING, (xd & 0x80) == 0x80);
+
+        setInvisible((xd & 0x20) == 0x20);
+    }
+
+    /**
+     * Set a boolean - whether the entity is invisible or visible
+     *
+     * @param value true if the entity is invisible
+     */
+    protected void setInvisible(boolean value) {
+        setFlag(EntityFlag.INVISIBLE, value);
+    }
+
+    /**
+     * Set an int from 0 - this entity's maximum air - (air / maxAir) represents the percentage of bubbles left
+     */
+    public final void setAir(EntityMetadata<?> entityMetadata) {
+        setAir(((IntEntityMetadata) entityMetadata).getPrimitiveValue());
+    }
+
+    protected void setAir(int amount) {
+        dirtyMetadata.put(EntityData.AIR_SUPPLY, (short) MathUtils.constrain(amount, 0, getMaxAir()));
+    }
+
+    protected int getMaxAir() {
+        return 300;
+    }
+
+    public void setDisplayName(EntityMetadata<Component> entityMetadata) {
+        Component name = entityMetadata.getValue();
+        if (name != null) {
+            String displayName = MessageTranslator.convertMessage(name, session.getLocale());
+            dirtyMetadata.put(EntityData.NAMETAG, displayName);
+        } else if (!dirtyMetadata.getString(EntityData.NAMETAG).isEmpty()) { //TODO
+            // Clear nametag
+            dirtyMetadata.put(EntityData.NAMETAG, "");
+        }
+    }
+
+    public void setDisplayNameVisible(EntityMetadata<Boolean> entityMetadata) {
+        dirtyMetadata.put(EntityData.NAMETAG_ALWAYS_SHOW, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0));
+    }
+
+    public void setGravity(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.HAS_GRAVITY, !((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
+    }
+
+    /**
+     * Usually used for bounding box and not animation.
+     */
+    public void setPose(EntityMetadata<Pose> entityMetadata) {
+        Pose pose = entityMetadata.getValue();
+
+        setFlag(EntityFlag.SLEEPING, pose.equals(Pose.SLEEPING));
+        // Triggered when crawling
+        setFlag(EntityFlag.SWIMMING, pose.equals(Pose.SWIMMING));
+        setDimensions(pose);
     }
 
     /**
@@ -323,38 +376,33 @@ public class Entity {
      */
     protected void setDimensions(Pose pose) {
         // No flexibility options for basic entities
-        //TODO don't even set this for basic entities since we already set it on entity initialization
-        metadata.put(EntityData.BOUNDING_BOX_WIDTH, entityType.getWidth());
-        metadata.put(EntityData.BOUNDING_BOX_HEIGHT, entityType.getHeight());
+        if (boundingBoxHeight != definition.height() || boundingBoxWidth != definition.width()) {
+            boundingBoxWidth = definition.width();
+            boundingBoxHeight = definition.height();
+            dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, boundingBoxWidth);
+            dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, boundingBoxHeight);
+        }
     }
 
     /**
      * Set a float from 0-1 - how strong the "frozen" overlay should be on screen.
      */
-    protected void setFreezing(GeyserSession session, float amount) {
-        metadata.put(EntityData.FREEZING_EFFECT_STRENGTH, amount);
+    public float setFreezing(EntityMetadata<Integer> entityMetadata) {
+        // The value that Java edition gives us is in ticks, but Bedrock uses a float percentage of the strength 0.0 -> 1.0
+        // The Java client caps its freezing tick percentage at 140
+        int freezingTicks = Math.min(((IntEntityMetadata) entityMetadata).getPrimitiveValue(), 140);
+        float freezingPercentage = freezingTicks / 140f;
+        dirtyMetadata.put(EntityData.FREEZING_EFFECT_STRENGTH, freezingPercentage);
+        return freezingPercentage;
     }
 
     /**
-     * Set an int from 0 - this entity's maximum air - (air / maxAir) represents the percentage of bubbles left
-     * @param amount the amount of air
-     */
-    protected void setAir(int amount) {
-        metadata.put(EntityData.AIR_SUPPLY, (short) MathUtils.constrain(amount, 0, getMaxAir()));
-    }
-
-    protected int getMaxAir() {
-        return 300;
-    }
-
-    /**
-     * Set a boolean - whether the entity is invisible or visible
+     * If true, the entity should be shaking on the client's end.
      *
-     * @param session the Geyser session
-     * @param value true if the entity is invisible
+     * @return whether {@link EntityFlag#SHAKING} should be set to true.
      */
-    protected void setInvisible(GeyserSession session, boolean value) {
-        metadata.getFlags().setFlag(EntityFlag.INVISIBLE, value);
+    protected boolean isShaking() {
+        return false;
     }
 
     /**
@@ -363,7 +411,7 @@ public class Entity {
      * @return the bedrock rotation
      */
     public Vector3f getBedrockRotation() {
-        return Vector3f.from(rotation.getY(), rotation.getZ(), rotation.getX());
+        return Vector3f.from(pitch, headYaw, yaw);
     }
 
     @SuppressWarnings("unchecked")
diff --git a/connector/src/main/java/org/geysermc/connector/entity/EntityDefinition.java b/connector/src/main/java/org/geysermc/connector/entity/EntityDefinition.java
new file mode 100644
index 000000000..c4100d95d
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/entity/EntityDefinition.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.entity;
+
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.geysermc.connector.entity.factory.BaseEntityFactory;
+import org.geysermc.connector.entity.factory.EntityFactory;
+import org.geysermc.connector.registry.Registries;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.function.BiConsumer;
+
+/**
+ * Represents data for an entity. This includes properties such as height and width, as well as the list of entity
+ * metadata translators needed to translate the properties sent from the server. The translators are structured in such
+ * a way that inserting a new one (for example in version updates) is convenient.
+ *
+ * @param <T> the entity type this definition represents
+ */
+public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, EntityType entityType, int bedrockId, String identifier,
+                                                 float width, float height, float offset, List<EntityMetadataTranslator<? super T, ?>> translators) {
+
+    public static <T extends Entity> Builder<T> inherited(BaseEntityFactory<T> factory, EntityDefinition<? super T> parent) {
+        return inherited((EntityFactory<T>) factory, parent);
+    }
+
+    public static <T extends Entity> Builder<T> inherited(EntityFactory<T> factory, EntityDefinition<? super T> parent) {
+        return new Builder<>(factory, parent.entityType, parent.bedrockId, parent.identifier, parent.width, parent.height, parent.offset, new ObjectArrayList<>(parent.translators));
+    }
+
+    public static <T extends Entity> Builder<T> builder(EntityFactory<T> factory) {
+        return new Builder<>(factory);
+    }
+
+    @Setter
+    @Accessors(fluent = true, chain = true)
+    public static class Builder<T extends Entity> {
+        private final EntityFactory<T> factory;
+        private EntityType type;
+        private int bedrockId;
+        private String identifier;
+        private float width;
+        private float height;
+        private float offset;
+        private final List<EntityMetadataTranslator<? super T, ?>> translators;
+
+        private Builder(EntityFactory<T> factory) {
+            this.factory = factory;
+            translators = new ObjectArrayList<>();
+        }
+
+        public Builder(EntityFactory<T> factory, EntityType type, int bedrockId, String identifier, float width, float height, float offset, List<EntityMetadataTranslator<? super T, ?>> translators) {
+            this.factory = factory;
+            this.type = type;
+            this.bedrockId = bedrockId;
+            this.identifier = identifier;
+            this.width = width;
+            this.height = height;
+            this.offset = offset;
+            this.translators = translators;
+        }
+
+        /**
+         * Sets the height and width as one value
+         */
+        public Builder<T> heightAndWidth(float value) {
+            height = value;
+            width = value;
+            return this;
+        }
+
+        /**
+         * Resets the identifier as well
+         */
+        public Builder<T> type(EntityType type) {
+            this.type = type;
+            identifier = null;
+            return this;
+        }
+
+        public <U> Builder<T> addTranslator(MetadataType type, BiConsumer<T, EntityMetadata<U>> translateFunction) {
+            translators.add(new EntityMetadataTranslator<>(type, translateFunction));
+            return this;
+        }
+
+        public Builder<T> addTranslator(EntityMetadataTranslator<T, ?> translator) {
+            translators.add(translator);
+            return this;
+        }
+
+        public EntityDefinition<T> build() {
+            return build(true);
+        }
+
+        /**
+         * @param register whether to register this entity in the Registries for entity types. Generally this should be
+         *                 set to false if we're not expecting this entity to spawn from the network.
+         */
+        public EntityDefinition<T> build(boolean register) {
+            if (identifier == null && type != null) {
+                identifier = "minecraft:" + type.name().toLowerCase(Locale.ROOT);
+            }
+            EntityDefinition<T> definition = new EntityDefinition<>(factory, type, bedrockId, identifier, width, height, offset, translators);
+            if (register && definition.entityType() != null) {
+                Registries.ENTITY_DEFINITIONS.get().putIfAbsent(definition.entityType(), definition);
+                Registries.JAVA_ENTITY_IDENTIFIERS.get().putIfAbsent(definition.identifier(), definition);
+            }
+            return definition;
+        }
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/entity/EntityDefinitions.java b/connector/src/main/java/org/geysermc/connector/entity/EntityDefinitions.java
new file mode 100644
index 000000000..926c86c29
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/entity/EntityDefinitions.java
@@ -0,0 +1,999 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.entity;
+
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
+import com.nukkitx.protocol.bedrock.data.entity.EntityData;
+import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import net.kyori.adventure.text.Component;
+import org.geysermc.connector.entity.factory.BaseEntityFactory;
+import org.geysermc.connector.entity.factory.EntityFactory;
+import org.geysermc.connector.entity.factory.ExperienceOrbEntityFactory;
+import org.geysermc.connector.entity.factory.PaintingEntityFactory;
+import org.geysermc.connector.entity.living.*;
+import org.geysermc.connector.entity.living.animal.*;
+import org.geysermc.connector.entity.living.animal.horse.*;
+import org.geysermc.connector.entity.living.animal.tameable.CatEntity;
+import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity;
+import org.geysermc.connector.entity.living.animal.tameable.TameableEntity;
+import org.geysermc.connector.entity.living.animal.tameable.WolfEntity;
+import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity;
+import org.geysermc.connector.entity.living.merchant.VillagerEntity;
+import org.geysermc.connector.entity.living.monster.*;
+import org.geysermc.connector.entity.living.monster.raid.PillagerEntity;
+import org.geysermc.connector.entity.living.monster.raid.RaidParticipantEntity;
+import org.geysermc.connector.entity.living.monster.raid.SpellcasterIllagerEntity;
+import org.geysermc.connector.entity.living.monster.raid.VindicatorEntity;
+import org.geysermc.connector.entity.player.PlayerEntity;
+import org.geysermc.connector.network.translators.chat.MessageTranslator;
+
+public final class EntityDefinitions {
+    public static final EntityDefinition<AreaEffectCloudEntity> AREA_EFFECT_CLOUD;
+    public static final EntityDefinition<ArmorStandEntity> ARMOR_STAND;
+    public static final EntityDefinition<TippedArrowEntity> ARROW;
+    public static final EntityDefinition<AxolotlEntity> AXOLOTL;
+    public static final EntityDefinition<BatEntity> BAT;
+    public static final EntityDefinition<BeeEntity> BEE;
+    public static final EntityDefinition<BlazeEntity> BLAZE;
+    public static final EntityDefinition<BoatEntity> BOAT;
+    public static final EntityDefinition<CatEntity> CAT;
+    public static final EntityDefinition<MonsterEntity> CAVE_SPIDER;
+    public static final EntityDefinition<ChickenEntity> CHICKEN;
+    public static final EntityDefinition<CreeperEntity> CREEPER;
+    public static final EntityDefinition<AbstractFishEntity> COD;
+    public static final EntityDefinition<AnimalEntity> COW;
+    public static final EntityDefinition<ChestedHorseEntity> DONKEY;
+    public static final EntityDefinition<WaterEntity> DOLPHIN;
+    public static final EntityDefinition<ItemedFireballEntity> DRAGON_FIREBALL;
+    public static final EntityDefinition<ZombieEntity> DROWNED;
+    public static final EntityDefinition<ElderGuardianEntity> ELDER_GUARDIAN;
+    public static final EntityDefinition<EndermanEntity> ENDERMAN;
+    public static final EntityDefinition<MonsterEntity> ENDERMITE;
+    public static final EntityDefinition<EnderDragonEntity> ENDER_DRAGON;
+    public static final EntityDefinition<EnderCrystalEntity> END_CRYSTAL;
+    public static final EntityDefinition<SpellcasterIllagerEntity> EVOKER;
+    public static final EntityDefinition<Entity> EVOKER_FANGS;
+    public static final EntityDefinition<ExpOrbEntity> EXPERIENCE_ORB;
+    public static final EntityDefinition<Entity> EYE_OF_ENDER;
+    public static final EntityDefinition<FallingBlockEntity> FALLING_BLOCK;
+    public static final EntityDefinition<ItemedFireballEntity> FIREBALL;
+    public static final EntityDefinition<FishingHookEntity> FISHING_BOBBER;
+    public static final EntityDefinition<FireworkEntity> FIREWORK_ROCKET;
+    public static final EntityDefinition<FoxEntity> FOX;
+    public static final EntityDefinition<GiantEntity> GIANT;
+    public static final EntityDefinition<GhastEntity> GHAST;
+    public static final EntityDefinition<ItemFrameEntity> GLOW_ITEM_FRAME;
+    public static final EntityDefinition<GlowSquidEntity> GLOW_SQUID;
+    public static final EntityDefinition<GoatEntity> GOAT;
+    public static final EntityDefinition<GuardianEntity> GUARDIAN;
+    public static final EntityDefinition<HoglinEntity> HOGLIN;
+    public static final EntityDefinition<HorseEntity> HORSE;
+    public static final EntityDefinition<ZombieEntity> HUSK;
+    public static final EntityDefinition<SpellcasterIllagerEntity> ILLUSIONER; // Not present on Bedrock
+    public static final EntityDefinition<IronGolemEntity> IRON_GOLEM;
+    public static final EntityDefinition<ItemEntity> ITEM;
+    public static final EntityDefinition<ItemFrameEntity> ITEM_FRAME;
+    public static final EntityDefinition<LeashKnotEntity> LEASH_KNOT;
+    public static final EntityDefinition<LightningEntity> LIGHTNING_BOLT;
+    public static final EntityDefinition<LlamaEntity> LLAMA;
+    public static final EntityDefinition<ThrowableEntity> LLAMA_SPIT;
+    public static final EntityDefinition<MagmaCubeEntity> MAGMA_CUBE;
+    public static final EntityDefinition<MinecartEntity> MINECART;
+    public static final EntityDefinition<MinecartEntity> MINECART_CHEST;
+    public static final EntityDefinition<CommandBlockMinecartEntity> MINECART_COMMAND_BLOCK;
+    public static final EntityDefinition<MinecartEntity> MINECART_HOPPER;
+    public static final EntityDefinition<FurnaceMinecartEntity> MINECART_FURNACE; // Not present on Bedrock
+    public static final EntityDefinition<SpawnerMinecartEntity> MINECART_SPAWNER; // Not present on Bedrock
+    public static final EntityDefinition<MinecartEntity> MINECART_TNT;
+    public static final EntityDefinition<MooshroomEntity> MOOSHROOM;
+    public static final EntityDefinition<ChestedHorseEntity> MULE;
+    public static final EntityDefinition<OcelotEntity> OCELOT;
+    public static final EntityDefinition<PaintingEntity> PAINTING;
+    public static final EntityDefinition<PandaEntity> PANDA;
+    public static final EntityDefinition<ParrotEntity> PARROT;
+    public static final EntityDefinition<PhantomEntity> PHANTOM;
+    public static final EntityDefinition<PigEntity> PIG;
+    public static final EntityDefinition<PiglinEntity> PIGLIN;
+    public static final EntityDefinition<BasePiglinEntity> PIGLIN_BRUTE;
+    public static final EntityDefinition<PillagerEntity> PILLAGER;
+    public static final EntityDefinition<PlayerEntity> PLAYER;
+    public static final EntityDefinition<PolarBearEntity> POLAR_BEAR;
+    public static final EntityDefinition<TNTEntity> PRIMED_TNT;
+    public static final EntityDefinition<PufferFishEntity> PUFFERFISH;
+    public static final EntityDefinition<RabbitEntity> RABBIT;
+    public static final EntityDefinition<RaidParticipantEntity> RAVAGER;
+    public static final EntityDefinition<AbstractFishEntity> SALMON;
+    public static final EntityDefinition<SheepEntity> SHEEP;
+    public static final EntityDefinition<ShulkerEntity> SHULKER;
+    public static final EntityDefinition<ThrowableEntity> SHULKER_BULLET;
+    public static final EntityDefinition<MonsterEntity> SILVERFISH;
+    public static final EntityDefinition<SkeletonEntity> SKELETON;
+    public static final EntityDefinition<AbstractHorseEntity> SKELETON_HORSE;
+    public static final EntityDefinition<SlimeEntity> SLIME;
+    public static final EntityDefinition<ItemedFireballEntity> SMALL_FIREBALL;
+    public static final EntityDefinition<ThrowableItemEntity> SNOWBALL;
+    public static final EntityDefinition<SnowGolemEntity> SNOW_GOLEM;
+    public static final EntityDefinition<AbstractArrowEntity> SPECTRAL_ARROW;
+    public static final EntityDefinition<SpiderEntity> SPIDER;
+    public static final EntityDefinition<SquidEntity> SQUID;
+    public static final EntityDefinition<AbstractSkeletonEntity> STRAY;
+    public static final EntityDefinition<StriderEntity> STRIDER;
+    public static final EntityDefinition<ThrowableItemEntity> THROWN_EGG;
+    public static final EntityDefinition<ThrowableItemEntity> THROWN_ENDERPEARL;
+    public static final EntityDefinition<ThrowableItemEntity> THROWN_EXP_BOTTLE;
+    public static final EntityDefinition<ThrownPotionEntity> THROWN_POTION;
+    public static final EntityDefinition<TropicalFishEntity> TROPICAL_FISH;
+    public static final EntityDefinition<TurtleEntity> TURTLE;
+    public static final EntityDefinition<TraderLlamaEntity> TRADER_LLAMA;
+    public static final EntityDefinition<TridentEntity> TRIDENT;
+    public static final EntityDefinition<AbstractMerchantEntity> WANDERING_TRADER;
+    public static final EntityDefinition<RaidParticipantEntity> WITCH;
+    public static final EntityDefinition<WitherEntity> WITHER;
+    public static final EntityDefinition<AbstractSkeletonEntity> WITHER_SKELETON;
+    public static final EntityDefinition<WitherSkullEntity> WITHER_SKULL;
+    public static final EntityDefinition<WolfEntity> WOLF;
+    public static final EntityDefinition<VillagerEntity> VILLAGER;
+    public static final EntityDefinition<VindicatorEntity> VINDICATOR;
+    public static final EntityDefinition<VexEntity> VEX;
+    public static final EntityDefinition<ZoglinEntity> ZOGLIN;
+    public static final EntityDefinition<ZombieEntity> ZOMBIE;
+    public static final EntityDefinition<AbstractHorseEntity> ZOMBIE_HORSE;
+    public static final EntityDefinition<ZombieVillagerEntity> ZOMBIE_VILLAGER;
+    public static final EntityDefinition<ZombifiedPiglinEntity> ZOMBIFIED_PIGLIN;
+
+    /**
+     * Is not sent over the network
+     */
+    public static final EntityDefinition<EnderDragonPartEntity> ENDER_DRAGON_PART;
+    /**
+     * Special Bedrock type
+     */
+    public static final EntityDefinition<WitherSkullEntity> WITHER_SKULL_DANGEROUS;
+
+    static {
+        EntityDefinition<Entity> entityBase = EntityDefinition.builder((BaseEntityFactory<Entity>) Entity::new)
+                .addTranslator(MetadataType.BYTE, Entity::setFlags)
+                .addTranslator(MetadataType.INT, Entity::setAir) // Air/bubbles
+                .addTranslator(MetadataType.OPTIONAL_CHAT, Entity::setDisplayName)
+                .addTranslator(MetadataType.BOOLEAN, Entity::setDisplayNameVisible)
+                .<Boolean>addTranslator(MetadataType.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.SILENT, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                .addTranslator(MetadataType.BOOLEAN, Entity::setGravity)
+                .addTranslator(MetadataType.POSE, Entity::setPose)
+                .addTranslator(MetadataType.INT, Entity::setFreezing)
+                .build();
+
+        // Extends entity
+        {
+            AREA_EFFECT_CLOUD = EntityDefinition.inherited(AreaEffectCloudEntity::new, entityBase)
+                    .type(EntityType.AREA_EFFECT_CLOUD)
+                    .bedrockId(95)
+                    .height(0.5f).width(1.0f)
+                    .addTranslator(MetadataType.FLOAT, AreaEffectCloudEntity::setRadius)
+                    .addTranslator(MetadataType.INT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityData.EFFECT_COLOR, entityMetadata.getValue()))
+                    .addTranslator(null) // Waiting
+                    .addTranslator(MetadataType.PARTICLE, AreaEffectCloudEntity::setParticle)
+                    .build();
+            BOAT = EntityDefinition.inherited(BoatEntity::new, entityBase)
+                    .type(EntityType.BOAT)
+                    .bedrockId(90)
+                    .height(0.6f).width(1.6f)
+                    .offset(0.35f)
+                    .addTranslator(MetadataType.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityData.HURT_TIME, entityMetadata.getValue())) // Time since last hit
+                    .addTranslator(MetadataType.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityData.HURT_DIRECTION, entityMetadata.getValue())) // Rocking direction
+                    .<Float>addTranslator(MetadataType.FLOAT, (boatEntity, entityMetadata) ->
+                            // 'Health' in Bedrock, damage taken in Java - it makes motion in Bedrock
+                            boatEntity.getDirtyMetadata().put(EntityData.HEALTH, 40 - ((int) ((FloatEntityMetadata) entityMetadata).getPrimitiveValue())))
+                    .addTranslator(MetadataType.INT, BoatEntity::setVariant)
+                    .addTranslator(MetadataType.BOOLEAN, BoatEntity::setPaddlingLeft)
+                    .addTranslator(MetadataType.BOOLEAN, BoatEntity::setPaddlingRight)
+                    .addTranslator(MetadataType.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityData.BOAT_BUBBLE_TIME, entityMetadata.getValue())) // May not actually do anything
+                    .build();
+            DRAGON_FIREBALL = EntityDefinition.inherited(ItemedFireballEntity::new, entityBase)
+                    .type(EntityType.DRAGON_FIREBALL)
+                    .bedrockId(79)
+                    .heightAndWidth(1.0f)
+                    .build();
+            END_CRYSTAL = EntityDefinition.inherited(EnderCrystalEntity::new, entityBase)
+                    .type(EntityType.END_CRYSTAL)
+                    .bedrockId(71)
+                    .heightAndWidth(2.0f)
+                    .addTranslator(MetadataType.OPTIONAL_POSITION, EnderCrystalEntity::setBlockTarget)
+                    .<Boolean>addTranslator(MetadataType.BOOLEAN,
+                            (enderCrystalEntity, entityMetadata) -> enderCrystalEntity.setFlag(EntityFlag.SHOW_BOTTOM, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) // There is a base located on the ender crystal
+                    .build();
+            EXPERIENCE_ORB = EntityDefinition.inherited((ExperienceOrbEntityFactory) ExpOrbEntity::new, entityBase)
+                    .type(EntityType.EXPERIENCE_ORB)
+                    .bedrockId(69)
+                    .identifier("minecraft:xp_orb")
+                    .build();
+            EVOKER_FANGS = EntityDefinition.inherited(entityBase.factory(), entityBase)
+                    .type(EntityType.EVOKER_FANGS)
+                    .bedrockId(103)
+                    .height(0.8f).width(0.5f)
+                    .identifier("minecraft:evocation_fang")
+                    .build();
+            EYE_OF_ENDER = EntityDefinition.inherited(Entity::new, entityBase)
+                    .type(EntityType.EYE_OF_ENDER)
+                    .bedrockId(70)
+                    .heightAndWidth(0.25f)
+                    .identifier("minecraft:eye_of_ender_signal")
+                    .build();
+            FALLING_BLOCK = EntityDefinition.inherited(new EntityFactory<FallingBlockEntity>() {
+                    }, entityBase) // TODO
+                    .type(EntityType.FALLING_BLOCK)
+                    .bedrockId(66)
+                    .heightAndWidth(0.98f)
+                    .build();
+            FIREBALL = EntityDefinition.inherited(ItemedFireballEntity::new, entityBase)
+                    .type(EntityType.FIREBALL)
+                    .bedrockId(85)
+                    .heightAndWidth(1.0f)
+                    .build();
+            FIREWORK_ROCKET = EntityDefinition.inherited(FireworkEntity::new, entityBase)
+                    .type(EntityType.FIREWORK_ROCKET)
+                    .bedrockId(72)
+                    .heightAndWidth(0.25f)
+                    .identifier("minecraft:fireworks_rocket")
+                    .addTranslator(MetadataType.ITEM, FireworkEntity::setFireworkItem)
+                    .addTranslator(MetadataType.OPTIONAL_VARINT, FireworkEntity::setPlayerGliding)
+                    .build();
+            FISHING_BOBBER = EntityDefinition.inherited(new EntityFactory<FishingHookEntity>() {
+                    }, entityBase) //TODO
+                    .type(EntityType.FISHING_BOBBER)
+                    .bedrockId(77)
+                    .identifier("minecraft:fishing_book")
+                    .addTranslator(MetadataType.INT, FishingHookEntity::setHookedEntity)
+                    .build();
+            ITEM = EntityDefinition.inherited(ItemEntity::new, entityBase)
+                    .type(EntityType.ITEM)
+                    .bedrockId(64)
+                    .heightAndWidth(0.25f)
+                    .offset(0.125f)
+                    .addTranslator(MetadataType.ITEM, ItemEntity::setItem)
+                    .build();
+            LEASH_KNOT = EntityDefinition.inherited(LeashKnotEntity::new, entityBase)
+                    .type(EntityType.LEASH_KNOT)
+                    .bedrockId(88)
+                    .height(0.5f).width(0.375f)
+                    .build();
+            LIGHTNING_BOLT = EntityDefinition.inherited(LightningEntity::new, entityBase)
+                    .type(EntityType.LIGHTNING_BOLT)
+                    .bedrockId(93)
+                    .build();
+            LLAMA_SPIT = EntityDefinition.inherited(ThrowableEntity::new, entityBase)
+                    .type(EntityType.LLAMA_SPIT)
+                    .bedrockId(102)
+                    .heightAndWidth(0.25f)
+                    .build();
+            PAINTING = EntityDefinition.inherited((PaintingEntityFactory) PaintingEntity::new, entityBase)
+                    .type(EntityType.PAINTING)
+                    .bedrockId(83)
+                    .build();
+            PRIMED_TNT = EntityDefinition.inherited(TNTEntity::new, entityBase)
+                    .type(EntityType.PRIMED_TNT)
+                    .bedrockId(65)
+                    .heightAndWidth(0.98f)
+                    .identifier("minecraft:tnt")
+                    .addTranslator(MetadataType.INT, TNTEntity::setFuseLength)
+                    .build();
+            SHULKER_BULLET = EntityDefinition.inherited(ThrowableEntity::new, entityBase)
+                    .type(EntityType.SHULKER_BULLET)
+                    .bedrockId(76)
+                    .heightAndWidth(0.3125f)
+                    .build();
+            SMALL_FIREBALL = EntityDefinition.inherited(ItemedFireballEntity::new, entityBase)
+                    .type(EntityType.SMALL_FIREBALL)
+                    .bedrockId(94)
+                    .heightAndWidth(0.3125f)
+                    .build();
+            SNOWBALL = EntityDefinition.inherited(ThrowableItemEntity::new, entityBase)
+                    .type(EntityType.SNOWBALL)
+                    .bedrockId(81)
+                    .heightAndWidth(0.25f)
+                    .build();
+            THROWN_ENDERPEARL = EntityDefinition.inherited(ThrowableItemEntity::new, entityBase)
+                    .type(EntityType.THROWN_ENDERPEARL)
+                    .bedrockId(87)
+                    .heightAndWidth(0.25f)
+                    .identifier("minecraft:ender_pearl")
+                    .build();
+            THROWN_EGG = EntityDefinition.inherited(ThrowableItemEntity::new, entityBase)
+                    .type(EntityType.THROWN_EGG)
+                    .bedrockId(82)
+                    .heightAndWidth(0.25f)
+                    .identifier("minecraft:egg")
+                    .build();
+            THROWN_EXP_BOTTLE = EntityDefinition.inherited(ThrowableItemEntity::new, entityBase)
+                    .type(EntityType.THROWN_EXP_BOTTLE)
+                    .bedrockId(68)
+                    .heightAndWidth(0.25f)
+                    .identifier("minecraft:xp_bottle")
+                    .build();
+            THROWN_POTION = EntityDefinition.inherited(ThrownPotionEntity::new, entityBase)
+                    .type(EntityType.THROWN_POTION)
+                    .bedrockId(86)
+                    .heightAndWidth(0.25f)
+                    .identifier("minecraft:splash_potion")
+                    .addTranslator(MetadataType.ITEM, ThrownPotionEntity::setPotion)
+                    .build();
+
+            EntityDefinition<AbstractArrowEntity> abstractArrowBase = EntityDefinition.inherited(AbstractArrowEntity::new, entityBase)
+                    .addTranslator(MetadataType.BYTE, AbstractArrowEntity::setArrowFlags)
+                    .addTranslator(null) // "Piercing level"
+                    .build();
+            ARROW = EntityDefinition.inherited(TippedArrowEntity::new, abstractArrowBase)
+                    .type(EntityType.ARROW)
+                    .bedrockId(80)
+                    .heightAndWidth(0.25f)
+                    .addTranslator(MetadataType.INT, TippedArrowEntity::setPotionEffectColor)
+                    .build();
+            SPECTRAL_ARROW = EntityDefinition.inherited(abstractArrowBase.factory(), abstractArrowBase)
+                    .type(EntityType.SPECTRAL_ARROW)
+                    .bedrockId(80)
+                    .heightAndWidth(0.25f)
+                    .identifier("minecraft:arrow")
+                    .build();
+            TRIDENT = EntityDefinition.inherited(TridentEntity::new, abstractArrowBase) // TODO remove class
+                    .type(EntityType.TRIDENT)
+                    .bedrockId(73)
+                    .identifier("minecraft:thrown_trident")
+                    .addTranslator(null) // Loyalty
+                    .<Boolean>addTranslator(MetadataType.BOOLEAN, (tridentEntity, entityMetadata) -> tridentEntity.setFlag(EntityFlag.ENCHANTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                    .build();
+
+            // Item frames are handled differently as they are blocks, not items, in Bedrock
+            ITEM_FRAME = EntityDefinition.inherited(new EntityFactory<ItemFrameEntity>() {
+                    }, entityBase) // TODO
+                    .type(EntityType.ITEM_FRAME)
+                    .addTranslator(MetadataType.ITEM, ItemFrameEntity::setItemInFrame)
+                    .addTranslator(MetadataType.ITEM, ItemFrameEntity::setItemRotation)
+                    .build();
+            GLOW_ITEM_FRAME = EntityDefinition.inherited(ITEM_FRAME.factory(), ITEM_FRAME)
+                    .type(EntityType.GLOW_ITEM_FRAME)
+                    .build();
+
+            MINECART = EntityDefinition.inherited(MinecartEntity::new, entityBase)
+                    .type(EntityType.MINECART)
+                    .bedrockId(84)
+                    .height(0.7f).width(0.98f)
+                    .offset(0.35f)
+                    .addTranslator(MetadataType.INT, (minecartEntity, entityMetadata) -> minecartEntity.getDirtyMetadata().put(EntityData.HEALTH, entityMetadata.getValue()))
+                    .addTranslator(MetadataType.INT, (minecartEntity, entityMetadata) -> minecartEntity.getDirtyMetadata().put(EntityData.HURT_DIRECTION, entityMetadata.getValue())) // Direction in which the minecart is shaking
+                    .<Float>addTranslator(MetadataType.FLOAT, (minecartEntity, entityMetadata) ->
+                            // Power in Java, time in Bedrock
+                            minecartEntity.getDirtyMetadata().put(EntityData.HURT_TIME, Math.min((int) ((FloatEntityMetadata) entityMetadata).getPrimitiveValue(), 15)))
+                    .addTranslator(MetadataType.BLOCK_STATE, MinecartEntity::setCustomBlock)
+                    .addTranslator(MetadataType.INT, MinecartEntity::setCustomBlockOffset)
+                    .addTranslator(MetadataType.BOOLEAN, MinecartEntity::setShowCustomBlock)
+                    .build();
+            MINECART_CHEST = EntityDefinition.inherited(MINECART.factory(), MINECART)
+                    .type(EntityType.MINECART_CHEST)
+                    .identifier("minecraft:chest_minecart")
+                    .build();
+            MINECART_COMMAND_BLOCK = EntityDefinition.inherited(CommandBlockMinecartEntity::new, MINECART)
+                    .type(EntityType.MINECART_COMMAND_BLOCK)
+                    .bedrockId(100)
+                    .identifier("minecraft:command_block_minecart")
+                    .addTranslator(MetadataType.STRING, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityData.COMMAND_BLOCK_COMMAND, entityMetadata.getValue()))
+                    .<Component>addTranslator(MetadataType.CHAT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityData.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue())))
+                    .build();
+            MINECART_FURNACE = EntityDefinition.inherited(FurnaceMinecartEntity::new, MINECART)
+                    .type(EntityType.MINECART_FURNACE)
+                    .identifier("minecraft:minecart")
+                    .addTranslator(MetadataType.BOOLEAN, FurnaceMinecartEntity::setHasFuel)
+                    .build();
+            MINECART_HOPPER = EntityDefinition.inherited(MINECART.factory(), MINECART)
+                    .type(EntityType.MINECART_HOPPER)
+                    .identifier("minecraft:hopper_minecart")
+                    .build();
+            MINECART_SPAWNER = EntityDefinition.inherited(SpawnerMinecartEntity::new, MINECART)
+                    .type(EntityType.MINECART_SPAWNER)
+                    .identifier("minecraft:minecart")
+                    .build();
+            MINECART_TNT = EntityDefinition.inherited(MINECART.factory(), MINECART)
+                    .type(EntityType.MINECART_TNT)
+                    .identifier("minecraft:tnt_minecart")
+                    .build();
+
+            WITHER_SKULL = EntityDefinition.inherited(WitherSkullEntity::new, entityBase)
+                    .type(EntityType.WITHER_SKULL)
+                    .bedrockId(89)
+                    .heightAndWidth(0.3125f)
+                    .addTranslator(MetadataType.BOOLEAN, WitherSkullEntity::setDangerous)
+                    .build();
+            WITHER_SKULL_DANGEROUS = EntityDefinition.inherited(WITHER_SKULL.factory(), WITHER_SKULL)
+                    .bedrockId(91)
+                    .build(false);
+        }
+
+        EntityDefinition<LivingEntity> livingEntityBase = EntityDefinition.inherited(LivingEntity::new, entityBase)
+                .addTranslator(MetadataType.BYTE, LivingEntity::setLivingEntityFlags)
+                .addTranslator(MetadataType.FLOAT, LivingEntity::setHealth)
+                .<Float>addTranslator(MetadataType.FLOAT,
+                        (livingEntity, entityMetadata) -> livingEntity.getDirtyMetadata().put(EntityData.EFFECT_COLOR, entityMetadata.getValue()))
+                .<Boolean>addTranslator(MetadataType.BOOLEAN,
+                        (livingEntity, entityMetadata) -> livingEntity.getDirtyMetadata().put(EntityData.EFFECT_AMBIENT, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0)))
+                .addTranslator(null) // Arrow count
+                .addTranslator(null) // Stinger count
+                .addTranslator(MetadataType.POSITION, LivingEntity::setBedPosition)
+                .build();
+
+        ARMOR_STAND = EntityDefinition.inherited(ArmorStandEntity::new, livingEntityBase)
+                .type(EntityType.ARMOR_STAND)
+                .bedrockId(61)
+                .height(1.975f).width(0.5f)
+                .addTranslator(MetadataType.BYTE, ArmorStandEntity::setArmorStandFlags)
+                .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setHeadRotation)
+                .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setBodyRotation)
+                .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setLeftArmRotation)
+                .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setRightArmRotation)
+                .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setLeftLegRotation)
+                .addTranslator(MetadataType.ROTATION, ArmorStandEntity::setRightLegRotation)
+                .build();
+        PLAYER = EntityDefinition.<PlayerEntity>inherited(null, livingEntityBase)
+                .type(EntityType.PLAYER)
+                .bedrockId(63)
+                .height(1.8f).width(0.6f)
+                .offset(1.62f)
+                .addTranslator(MetadataType.FLOAT, PlayerEntity::setAbsorptionHearts)
+                .addTranslator(null) // Player score
+                .addTranslator(MetadataType.BYTE, PlayerEntity::setSkinVisibility)
+                .addTranslator(null) // Player main hand
+                .addTranslator(MetadataType.NBT_TAG, PlayerEntity::setLeftParrot)
+                .addTranslator(MetadataType.NBT_TAG, PlayerEntity::setRightParrot)
+                .build();
+
+        EntityDefinition<MobEntity> mobEntityBase = EntityDefinition.inherited(MobEntity::new, livingEntityBase)
+                .addTranslator(MetadataType.BYTE, MobEntity::setMobFlags)
+                .build();
+
+        // Extends mob
+        {
+            BAT = EntityDefinition.inherited(BatEntity::new, mobEntityBase)
+                    .type(EntityType.BAT)
+                    .bedrockId(19)
+                    .height(0.9f).width(0.5f)
+                    .addTranslator(MetadataType.BYTE, BatEntity::setBatFlags)
+                    .build();
+            BLAZE = EntityDefinition.inherited(BlazeEntity::new, mobEntityBase)
+                    .type(EntityType.BLAZE)
+                    .bedrockId(43)
+                    .height(1.8f).width(0.6f)
+                    .addTranslator(MetadataType.BYTE, BlazeEntity::setBlazeFlags)
+                    .build();
+            CAVE_SPIDER = EntityDefinition.inherited(MonsterEntity::new, mobEntityBase)
+                    .type(EntityType.CAVE_SPIDER)
+                    .bedrockId(40)
+                    .height(0.5f).width(0.7f)
+                    .build();
+            CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase)
+                    .type(EntityType.CREEPER)
+                    .bedrockId(33)
+                    .height(1.7f).width(0.6f)
+                    .offset(1.62f)
+                    .addTranslator(MetadataType.INT, CreeperEntity::setSwelling)
+                    .<Boolean>addTranslator(MetadataType.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.POWERED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                    .addTranslator(MetadataType.BOOLEAN, CreeperEntity::setIgnited)
+                    .build();
+            DOLPHIN = EntityDefinition.inherited(WaterEntity::new, mobEntityBase)
+                    .type(EntityType.DOLPHIN)
+                    .bedrockId(31)
+                    .height(0.6f).width(0.9f)
+                    //TODO check
+                    .addTranslator(null) // treasure position
+                    .addTranslator(null) // "got fish"
+                    .addTranslator(null) // "moistness level"
+                    .build();
+            ENDERMAN = EntityDefinition.inherited(EndermanEntity::new, mobEntityBase)
+                    .type(EntityType.ENDERMAN)
+                    .bedrockId(38)
+                    .height(2.9f).width(0.6f)
+                    .addTranslator(MetadataType.BLOCK_STATE, EndermanEntity::setCarriedBlock)
+                    .addTranslator(MetadataType.BOOLEAN, EndermanEntity::setScreaming)
+                    .addTranslator(MetadataType.BOOLEAN, EndermanEntity::setAngry)
+                    .build();
+            ENDERMITE = EntityDefinition.inherited(MonsterEntity::new, mobEntityBase)
+                    .type(EntityType.ENDERMITE)
+                    .bedrockId(55)
+                    .height(0.3f).width(0.4f)
+                    .build();
+            ENDER_DRAGON = EntityDefinition.inherited(EnderDragonEntity::new, mobEntityBase)
+                    .type(EntityType.ENDER_DRAGON)
+                    .bedrockId(53)
+                    .addTranslator(MetadataType.INT, EnderDragonEntity::setPhase)
+                    .build();
+            GHAST = EntityDefinition.inherited(GhastEntity::new, mobEntityBase)
+                    .type(EntityType.GHAST)
+                    .bedrockId(41)
+                    .heightAndWidth(4.0f)
+                    .addTranslator(MetadataType.BOOLEAN, GhastEntity::setGhastAttacking)
+                    .build();
+            GIANT = EntityDefinition.inherited(GiantEntity::new, mobEntityBase)
+                    .type(EntityType.GIANT)
+                    .bedrockId(32)
+                    .height(1.8f).width(1.6f)
+                    .offset(1.62f)
+                    .identifier("minecraft:zombie")
+                    .build();
+            IRON_GOLEM = EntityDefinition.inherited(IronGolemEntity::new, mobEntityBase)
+                    .type(EntityType.IRON_GOLEM)
+                    .bedrockId(20)
+                    .height(2.7f).width(1.4f)
+                    .build();
+            PHANTOM = EntityDefinition.inherited(PhantomEntity::new, mobEntityBase)
+                    .type(EntityType.PHANTOM)
+                    .bedrockId(58)
+                    .height(0.5f).width(0.9f)
+                    .offset(0.6f)
+                    .addTranslator(MetadataType.INT, PhantomEntity::setPhantomScale)
+                    .build();
+            SILVERFISH = EntityDefinition.inherited(MonsterEntity::new, mobEntityBase)
+                    .type(EntityType.SILVERFISH)
+                    .bedrockId(39)
+                    .height(0.3f).width(0.4f)
+                    .build();
+            SHULKER = EntityDefinition.inherited(ShulkerEntity::new, mobEntityBase)
+                    .type(EntityType.SHULKER)
+                    .bedrockId(54)
+                    .heightAndWidth(1f)
+                    .addTranslator(MetadataType.DIRECTION, ShulkerEntity::setAttachedFace)
+                    .addTranslator(MetadataType.BYTE, ShulkerEntity::setShulkerHeight)
+                    .addTranslator(MetadataType.BYTE, ShulkerEntity::setShulkerColor)
+                    .build();
+            SKELETON = EntityDefinition.inherited(SkeletonEntity::new, mobEntityBase)
+                    .type(EntityType.SKELETON)
+                    .bedrockId(34)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .addTranslator(MetadataType.BOOLEAN, SkeletonEntity::setConvertingToStray)
+                    .build();
+            SNOW_GOLEM = EntityDefinition.inherited(SnowGolemEntity::new, mobEntityBase)
+                    .type(EntityType.SNOW_GOLEM)
+                    .bedrockId(21)
+                    .height(1.9f).width(0.7f)
+                    .addTranslator(MetadataType.BYTE, SnowGolemEntity::setSnowGolemFlags)
+                    .build();
+            SPIDER = EntityDefinition.inherited(SpiderEntity::new, mobEntityBase)
+                    .type(EntityType.SPIDER)
+                    .bedrockId(35)
+                    .height(0.9f).width(1.4f)
+                    .offset(1f)
+                    .addTranslator(MetadataType.BYTE, SpiderEntity::setSpiderFlags)
+                    .build();
+            SQUID = EntityDefinition.inherited(SquidEntity::new, mobEntityBase)
+                    .type(EntityType.SQUID)
+                    .bedrockId(17)
+                    .heightAndWidth(0.8f)
+                    .build();
+            STRAY = EntityDefinition.inherited(AbstractSkeletonEntity::new, mobEntityBase)
+                    .type(EntityType.STRAY)
+                    .bedrockId(46)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .build();
+            VEX = EntityDefinition.inherited(VexEntity::new, mobEntityBase)
+                    .type(EntityType.VEX)
+                    .bedrockId(105)
+                    .height(0.8f).width(0.4f)
+                    .addTranslator(MetadataType.BYTE, VexEntity::setVexFlags)
+                    .build();
+            WITHER = EntityDefinition.inherited(WitherEntity::new, mobEntityBase)
+                    .type(EntityType.WITHER)
+                    .bedrockId(52)
+                    .height(3.5f).width(0.9f)
+                    .addTranslator(MetadataType.INT, WitherEntity::setTarget1)
+                    .addTranslator(MetadataType.INT, WitherEntity::setTarget2)
+                    .addTranslator(MetadataType.INT, WitherEntity::setTarget3)
+                    .addTranslator(MetadataType.INT, WitherEntity::setInvulnerableTicks)
+                    .build();
+            WITHER_SKELETON = EntityDefinition.inherited(AbstractSkeletonEntity::new, mobEntityBase)
+                    .type(EntityType.WITHER_SKELETON)
+                    .bedrockId(48)
+                    .height(2.4f).width(0.7f)
+                    .build();
+            ZOGLIN = EntityDefinition.inherited(ZoglinEntity::new, mobEntityBase)
+                    .type(EntityType.ZOGLIN)
+                    .bedrockId(126)
+                    .height(1.4f).width(1.3965f)
+                    .addTranslator(MetadataType.BOOLEAN, ZoglinEntity::setBaby)
+                    .build();
+            ZOMBIE = EntityDefinition.inherited(ZombieEntity::new, mobEntityBase)
+                    .type(EntityType.ZOMBIE)
+                    .bedrockId(32)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .addTranslator(MetadataType.BOOLEAN, ZombieEntity::setZombieBaby)
+                    .addTranslator(null) // "set special type", doesn't do anything
+                    .addTranslator(MetadataType.BOOLEAN, ZombieEntity::setConvertingToDrowned)
+                    .build();
+            ZOMBIE_VILLAGER = EntityDefinition.inherited(ZombieVillagerEntity::new, ZOMBIE)
+                    .type(EntityType.ZOMBIE_VILLAGER)
+                    .bedrockId(44)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .identifier("minecraft:zombie_villager_v2")
+                    .addTranslator(MetadataType.BOOLEAN, ZombieVillagerEntity::setTransforming)
+                    .addTranslator(MetadataType.VILLAGER_DATA, ZombieVillagerEntity::setZombieVillagerData)
+                    .build();
+            ZOMBIFIED_PIGLIN = EntityDefinition.inherited(ZombifiedPiglinEntity::new, ZOMBIE) //TODO test how zombie entity metadata is handled?
+                    .type(EntityType.ZOMBIFIED_PIGLIN)
+                    .bedrockId(36)
+                    .height(1.95f).width(0.6f)
+                    .offset(1.62f)
+                    .identifier("minecraft:zombie_pigman")
+                    .build();
+
+            DROWNED = EntityDefinition.inherited(ZOMBIE.factory(), ZOMBIE)
+                    .type(EntityType.DROWNED)
+                    .bedrockId(110)
+                    .height(1.95f).width(0.6f)
+                    .build();
+            HUSK = EntityDefinition.inherited(ZOMBIE.factory(), ZOMBIE)
+                    .type(EntityType.HUSK)
+                    .bedrockId(47)
+                    .build();
+
+            GUARDIAN = EntityDefinition.inherited(GuardianEntity::new, mobEntityBase)
+                    .type(EntityType.GUARDIAN)
+                    .bedrockId(49)
+                    .heightAndWidth(0.85f)
+                    .addTranslator(null) // Moving //TODO
+                    .addTranslator(MetadataType.INT, GuardianEntity::setGuardianTarget)
+                    .build();
+            ELDER_GUARDIAN = EntityDefinition.inherited(ElderGuardianEntity::new, GUARDIAN)
+                    .type(EntityType.ELDER_GUARDIAN)
+                    .bedrockId(50)
+                    .heightAndWidth(1.9975f)
+                    .build();
+
+            SLIME = EntityDefinition.inherited(SlimeEntity::new, mobEntityBase)
+                    .type(EntityType.SLIME)
+                    .bedrockId(37)
+                    .heightAndWidth(0.51f)
+                    .addTranslator(MetadataType.INT, SlimeEntity::setScale)
+                    .build();
+            MAGMA_CUBE = EntityDefinition.inherited(MagmaCubeEntity::new, SLIME)
+                    .type(EntityType.MAGMA_CUBE)
+                    .bedrockId(42)
+                    .build();
+
+            EntityDefinition<AbstractFishEntity> abstractFishEntityBase = EntityDefinition.inherited(AbstractFishEntity::new, mobEntityBase)
+                    .addTranslator(null) // From bucket
+                    .build();
+            COD = EntityDefinition.inherited(abstractFishEntityBase.factory(), abstractFishEntityBase)
+                    .type(EntityType.COD)
+                    .bedrockId(112)
+                    .height(0.25f).width(0.5f)
+                    .build();
+            PUFFERFISH = EntityDefinition.inherited(PufferFishEntity::new, abstractFishEntityBase)
+                    .type(EntityType.PUFFERFISH)
+                    .bedrockId(108)
+                    .heightAndWidth(0.7f)
+                    .addTranslator(MetadataType.INT, PufferFishEntity::setPufferfishSize)
+                    .build();
+            SALMON = EntityDefinition.inherited(abstractFishEntityBase.factory(), abstractFishEntityBase)
+                    .type(EntityType.SALMON)
+                    .bedrockId(109)
+                    .height(0.5f).width(0.7f)
+                    .build();
+            TROPICAL_FISH = EntityDefinition.inherited(TropicalFishEntity::new, abstractFishEntityBase)
+                    .type(EntityType.TROPICAL_FISH)
+                    .bedrockId(111)
+                    .heightAndWidth(0.6f)
+                    .identifier("minecraft:tropicalfish")
+                    .build();
+
+            EntityDefinition<BasePiglinEntity> abstractPiglinEntityBase = EntityDefinition.inherited(BasePiglinEntity::new, mobEntityBase)
+                    .addTranslator(MetadataType.BOOLEAN, BasePiglinEntity::setImmuneToZombification)
+                    .build();
+            PIGLIN = EntityDefinition.inherited(PiglinEntity::new, abstractPiglinEntityBase)
+                    .type(EntityType.PIGLIN)
+                    .bedrockId(123)
+                    .height(1.95f).width(0.6f)
+                    .addTranslator(MetadataType.BOOLEAN, PiglinEntity::setBaby)
+                    .addTranslator(MetadataType.BOOLEAN, PiglinEntity::setChargingCrossbow)
+                    .addTranslator(MetadataType.BOOLEAN, PiglinEntity::setDancing)
+                    .build();
+            PIGLIN_BRUTE = EntityDefinition.inherited(abstractPiglinEntityBase.factory(), abstractPiglinEntityBase)
+                    .type(EntityType.PIGLIN_BRUTE)
+                    .bedrockId(127)
+                    .height(1.95f).width(0.6f)
+                    .build();
+
+            GLOW_SQUID = EntityDefinition.inherited(GlowSquidEntity::new, SQUID)
+                    .type(EntityType.GLOW_SQUID)
+                    .addTranslator(null) // Set dark ticks remaining, possible TODO
+                    .build();
+
+            EntityDefinition<RaidParticipantEntity> raidParticipantEntityBase = EntityDefinition.inherited(RaidParticipantEntity::new, mobEntityBase)
+                    .addTranslator(null) // Celebrating //TODO
+                    .build();
+            EntityDefinition<SpellcasterIllagerEntity> spellcasterEntityBase = EntityDefinition.inherited(SpellcasterIllagerEntity::new, raidParticipantEntityBase)
+                    .addTranslator(MetadataType.BYTE, SpellcasterIllagerEntity::setSpellType)
+                    .build();
+            EVOKER = EntityDefinition.inherited(spellcasterEntityBase.factory(), spellcasterEntityBase)
+                    .type(EntityType.EVOKER)
+                    .bedrockId(104)
+                    .height(1.95f).width(0.6f)
+                    .identifier("minecraft:evocation_illager")
+                    .build();
+            ILLUSIONER = EntityDefinition.inherited(spellcasterEntityBase.factory(), spellcasterEntityBase)
+                    .type(EntityType.ILLUSIONER)
+                    .bedrockId(104)
+                    .height(1.95f).width(0.6f)
+                    .identifier("minecraft:evocation_illager")
+                    .build();
+            PILLAGER = EntityDefinition.inherited(PillagerEntity::new, raidParticipantEntityBase)
+                    .type(EntityType.PILLAGER)
+                    .bedrockId(114)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .addTranslator(null) // Charging; doesn't have an equivalent on Bedrock //TODO check
+                    .build();
+            RAVAGER = EntityDefinition.inherited(raidParticipantEntityBase.factory(), raidParticipantEntityBase)
+                    .type(EntityType.RAVAGER)
+                    .bedrockId(59)
+                    .height(1.9f).width(1.2f)
+                    .build();
+            VINDICATOR = EntityDefinition.inherited(VindicatorEntity::new, raidParticipantEntityBase)
+                    .type(EntityType.VINDICATOR)
+                    .bedrockId(57)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .build();
+            WITCH = EntityDefinition.inherited(raidParticipantEntityBase.factory(), raidParticipantEntityBase)
+                    .type(EntityType.WITCH)
+                    .bedrockId(45)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .build();
+        }
+
+        EntityDefinition<AgeableEntity> ageableEntityBase = EntityDefinition.inherited(AgeableEntity::new, mobEntityBase)
+                .addTranslator(MetadataType.BOOLEAN, AgeableEntity::setBaby)
+                .build();
+
+        // Extends ageable
+        {
+            AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase)
+                    .type(EntityType.AXOLOTL)
+                    .height(0.42f).width(0.7f)
+                    .addTranslator(MetadataType.INT, AxolotlEntity::setVariant)
+                    .addTranslator(MetadataType.BOOLEAN, AxolotlEntity::setPlayingDead)
+                    .build();
+            BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase)
+                    .type(EntityType.BEE)
+                    .bedrockId(122)
+                    .heightAndWidth(0.6f)
+                    .addTranslator(MetadataType.BYTE, BeeEntity::setBeeFlags)
+                    .addTranslator(MetadataType.INT, BeeEntity::setAngerTime)
+                    .build();
+            CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase)
+                    .type(EntityType.CHICKEN)
+                    .bedrockId(10)
+                    .height(0.7f).width(0.4f)
+                    .build();
+            COW = EntityDefinition.inherited(AnimalEntity::new, ageableEntityBase)
+                    .type(EntityType.COW)
+                    .bedrockId(11)
+                    .height(1.4f).width(0.9f)
+                    .build();
+            FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase)
+                    .type(EntityType.FOX)
+                    .bedrockId(121)
+                    .height(0.5f).width(1.25f)
+                    .addTranslator(MetadataType.INT, FoxEntity::setFoxVariant)
+                    .addTranslator(MetadataType.BYTE, FoxEntity::setFoxFlags)
+                    .build();
+            HOGLIN = EntityDefinition.inherited(HoglinEntity::new, ageableEntityBase)
+                    .type(EntityType.HOGLIN)
+                    .bedrockId(124)
+                    .height(1.4f).width(1.3965f)
+                    .addTranslator(MetadataType.BOOLEAN, HoglinEntity::setImmuneToZombification)
+                    .build();
+            GOAT = EntityDefinition.inherited(GoatEntity::new, ageableEntityBase)
+                    .type(EntityType.GOAT)
+                    .height(1.3f).width(0.9f)
+                    .addTranslator(MetadataType.BOOLEAN, GoatEntity::setScreamer)
+                    .build();
+            MOOSHROOM = EntityDefinition.inherited(MooshroomEntity::new, ageableEntityBase) // TODO remove class
+                    .type(EntityType.MOOSHROOM)
+                    .bedrockId(16)
+                    .height(1.4f).width(0.9f)
+                    .<String>addTranslator(MetadataType.STRING, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityData.VARIANT, entityMetadata.getValue().equals("brown") ? 1 : 0))
+                    .build();
+            OCELOT = EntityDefinition.inherited(OcelotEntity::new, ageableEntityBase)
+                    .type(EntityType.OCELOT)
+                    .bedrockId(22)
+                    .height(0.35f).width(0.3f)
+                    .<Boolean>addTranslator(MetadataType.BOOLEAN, (ocelotEntity, entityMetadata) -> ocelotEntity.setFlag(EntityFlag.TRUSTING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                    .build();
+            PANDA = EntityDefinition.inherited(PandaEntity::new, ageableEntityBase)
+                    .type(EntityType.PANDA)
+                    .bedrockId(113)
+                    .height(1.25f).width(1.125f)
+                    .addTranslator(null) // Unhappy counter
+                    .addTranslator(null) // Sneeze counter
+                    .addTranslator(MetadataType.INT, PandaEntity::setEatingCounter)
+                    .addTranslator(MetadataType.BYTE, PandaEntity::setMainGene)
+                    .addTranslator(MetadataType.BYTE, PandaEntity::setHiddenGene)
+                    .addTranslator(MetadataType.BYTE, PandaEntity::setPandaFlags)
+                    .build();
+            PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase)
+                    .type(EntityType.PIG)
+                    .bedrockId(12)
+                    .heightAndWidth(0.9f)
+                    .<Boolean>addTranslator(MetadataType.BOOLEAN, (pigEntity, entityMetadata) -> pigEntity.setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                    .addTranslator(null) // Boost time
+                    .build();
+            POLAR_BEAR = EntityDefinition.inherited(PolarBearEntity::new, ageableEntityBase)
+                    .type(EntityType.POLAR_BEAR)
+                    .bedrockId(28)
+                    .height(1.4f).width(1.3f)
+                    .<Boolean>addTranslator(MetadataType.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.STANDING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                    .build();
+            RABBIT = EntityDefinition.inherited(RabbitEntity::new, ageableEntityBase)
+                    .type(EntityType.RABBIT)
+                    .bedrockId(18)
+                    .height(0.5f).width(0.4f)
+                    .addTranslator(MetadataType.INT, RabbitEntity::setRabbitVariant)
+                    .build();
+            SHEEP = EntityDefinition.inherited(SheepEntity::new, ageableEntityBase)
+                    .type(EntityType.SHEEP)
+                    .bedrockId(13)
+                    .heightAndWidth(0.9f)
+                    .addTranslator(MetadataType.BYTE, SheepEntity::setSheepFlags)
+                    .build();
+            STRIDER = EntityDefinition.inherited(StriderEntity::new, ageableEntityBase)
+                    .type(EntityType.STRIDER)
+                    .bedrockId(125)
+                    .height(1.7f).width(0.9f)
+                    .addTranslator(null) // Boost time
+                    .addTranslator(MetadataType.BOOLEAN, StriderEntity::setCold)
+                    .addTranslator(MetadataType.BOOLEAN, StriderEntity::setSaddled)
+                    .build();
+            TURTLE = EntityDefinition.inherited(TurtleEntity::new, ageableEntityBase)
+                    .type(EntityType.TURTLE)
+                    .bedrockId(74)
+                    .height(0.4f).width(1.2f)
+                    .addTranslator(MetadataType.BOOLEAN, TurtleEntity::setPregnant)
+                    .addTranslator(MetadataType.BOOLEAN, TurtleEntity::setLayingEgg)
+                    .build();
+
+            EntityDefinition<AbstractMerchantEntity> abstractVillagerEntityBase = EntityDefinition.inherited(AbstractMerchantEntity::new, ageableEntityBase)
+                    .addTranslator(null) // Unhappy ticks
+                    .build();
+            VILLAGER = EntityDefinition.inherited(VillagerEntity::new, abstractVillagerEntityBase)
+                    .type(EntityType.VILLAGER)
+                    .bedrockId(15)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .identifier("minecraft:villager_v2")
+                    .addTranslator(MetadataType.VILLAGER_DATA, VillagerEntity::setVillagerData)
+                    .build();
+            WANDERING_TRADER = EntityDefinition.inherited(abstractVillagerEntityBase.factory(), abstractVillagerEntityBase)
+                    .type(EntityType.WANDERING_TRADER)
+                    .bedrockId(118)
+                    .height(1.8f).width(0.6f)
+                    .offset(1.62f)
+                    .build();
+        }
+
+        // Horses
+        {
+            EntityDefinition<AbstractHorseEntity> abstractHorseEntityBase = EntityDefinition.inherited(AbstractHorseEntity::new, ageableEntityBase)
+                    .addTranslator(MetadataType.BYTE, AbstractHorseEntity::setHorseFlags)
+                    .addTranslator(null) // UUID of owner
+                    .build();
+            HORSE = EntityDefinition.inherited(HorseEntity::new, abstractHorseEntityBase)
+                    .type(EntityType.HORSE)
+                    .bedrockId(23)
+                    .height(1.6f).width(1.3965f)
+                    .addTranslator(MetadataType.BYTE, HorseEntity::setHorseVariant)
+                    .build();
+            SKELETON_HORSE = EntityDefinition.inherited(abstractHorseEntityBase.factory(), abstractHorseEntityBase)
+                    .type(EntityType.SKELETON_HORSE)
+                    .bedrockId(26)
+                    .height(1.6f).width(1.3965f)
+                    .build();
+            ZOMBIE_HORSE = EntityDefinition.inherited(abstractHorseEntityBase.factory(), abstractHorseEntityBase)
+                    .type(EntityType.ZOMBIE_HORSE)
+                    .bedrockId(27)
+                    .height(1.6f).width(1.3965f)
+                    .build();
+            EntityDefinition<ChestedHorseEntity> chestedHorseEntityBase = EntityDefinition.inherited(ChestedHorseEntity::new, abstractHorseEntityBase)
+                    .<Boolean>addTranslator(MetadataType.BOOLEAN, (horseEntity, entityMetadata) -> horseEntity.setFlag(EntityFlag.CHESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                    .build();
+            DONKEY = EntityDefinition.inherited(chestedHorseEntityBase.factory(), chestedHorseEntityBase)
+                    .type(EntityType.DONKEY)
+                    .bedrockId(24)
+                    .height(1.6f).width(1.3965f)
+                    .build();
+            MULE = EntityDefinition.inherited(chestedHorseEntityBase.factory(), chestedHorseEntityBase)
+                    .type(EntityType.MULE)
+                    .bedrockId(25)
+                    .height(1.6f).width(1.3965f)
+                    .build();
+            LLAMA = EntityDefinition.inherited(LlamaEntity::new, chestedHorseEntityBase)
+                    .type(EntityType.LLAMA)
+                    .bedrockId(29)
+                    .height(1.87f).width(0.9f)
+                    .<Integer>addTranslator(MetadataType.INT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityData.STRENGTH, entityMetadata.getValue()))
+                    .addTranslator(MetadataType.INT, LlamaEntity::setCarpetedColor)
+                    .addTranslator(MetadataType.INT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityData.VARIANT, entityMetadata.getValue()))
+                    .build();
+            TRADER_LLAMA = EntityDefinition.inherited(TraderLlamaEntity::new, LLAMA)
+                    .type(EntityType.TRADER_LLAMA)
+                    .identifier("minecraft:llama")
+                    .build();
+        }
+
+        EntityDefinition<TameableEntity> tameableEntityBase = EntityDefinition.inherited(TameableEntity::new, ageableEntityBase)
+                .addTranslator(MetadataType.BYTE, TameableEntity::setTameableFlags)
+                .addTranslator(MetadataType.OPTIONAL_UUID, TameableEntity::setOwner)
+                .build();
+        CAT = EntityDefinition.inherited(CatEntity::new, tameableEntityBase)
+                .type(EntityType.CAT)
+                .bedrockId(75)
+                .height(0.35f).width(0.3f)
+                .addTranslator(MetadataType.INT, CatEntity::setCatVariant)
+                .addTranslator(MetadataType.BOOLEAN, CatEntity::setResting)
+                .addTranslator(null) // "resting state one" //TODO
+                .addTranslator(MetadataType.INT, CatEntity::setCollarColor)
+                .build();
+        PARROT = EntityDefinition.inherited(ParrotEntity::new, tameableEntityBase)
+                .type(EntityType.PARROT)
+                .bedrockId(30)
+                .height(0.9f).width(0.5f)
+                .addTranslator(MetadataType.INT, (parrotEntity, entityMetadata) -> parrotEntity.getDirtyMetadata().put(EntityData.VARIANT, entityMetadata.getValue())) // Parrot color
+                .build();
+        WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase)
+                .type(EntityType.WOLF)
+                .bedrockId(14)
+                .height(0.85f).width(0.6f)
+                // "Begging" on wiki.vg, "Interested" in Nukkit - the tilt of the head
+                .<Boolean>addTranslator(MetadataType.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
+                .addTranslator(MetadataType.INT, WolfEntity::setCollarColor)
+                .addTranslator(MetadataType.INT, WolfEntity::setWolfAngerTime)
+                .build();
+
+        // As of 1.18 these don't track entity data at all
+        ENDER_DRAGON_PART = EntityDefinition.<EnderDragonPartEntity>builder(null)
+                .bedrockId(32)
+                .identifier("minecraft:armor_stand") // Emulated
+                .build();
+    }
+
+    public static void init() {
+        // no-op
+    }
+
+    private EntityDefinitions() {
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/InsentientEntity.java b/connector/src/main/java/org/geysermc/connector/entity/EntityMetadataTranslator.java
similarity index 57%
rename from connector/src/main/java/org/geysermc/connector/entity/living/InsentientEntity.java
rename to connector/src/main/java/org/geysermc/connector/entity/EntityMetadataTranslator.java
index d0f99cdb9..e0936559d 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/InsentientEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/EntityMetadataTranslator.java
@@ -23,29 +23,18 @@
  * @link https://github.com/GeyserMC/Geyser
  */
 
-package org.geysermc.connector.entity.living;
+package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
-import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.LivingEntity;
-import org.geysermc.connector.entity.type.EntityType;
-import org.geysermc.connector.network.session.GeyserSession;
 
-public class InsentientEntity extends LivingEntity {
+import java.util.function.BiConsumer;
 
-    public InsentientEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
+/**
+ * Translates a given Java {@link EntityMetadata} into a similar/same construct for Bedrock
+ */
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 15 && entityMetadata.getType() == MetadataType.BYTE) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.NO_AI, (xd & 0x01) == 0x01);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
+public record EntityMetadataTranslator<E extends Entity, T>(
+        MetadataType acceptedType,
+        BiConsumer<E, EntityMetadata<T>> translateFunction) {
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/ExpOrbEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ExpOrbEntity.java
index f6fbcde9d..14dacc4cd 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/ExpOrbEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/ExpOrbEntity.java
@@ -27,22 +27,21 @@ package org.geysermc.connector.entity;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
 public class ExpOrbEntity extends Entity {
 
     private final int amount;
 
-    public ExpOrbEntity(int amount, long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ExpOrbEntity(GeyserSession session, int amount, long entityId, long geyserId, Vector3f position) {
+        super(session, entityId, geyserId, null, EntityDefinitions.EXPERIENCE_ORB, position, Vector3f.ZERO, 0, 0, 0);
 
         this.amount = amount;
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        this.metadata.put(EntityData.EXPERIENCE_VALUE, amount);
-        super.spawnEntity(session);
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        this.dirtyMetadata.put(EntityData.EXPERIENCE_VALUE, amount);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java
index a82118c21..5c5b4dde8 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java
@@ -26,31 +26,32 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class FallingBlockEntity extends Entity {
     private final int javaId;
 
-    public FallingBlockEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, int javaId) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public FallingBlockEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, Vector3f position, Vector3f motion, float yaw, float pitch, int javaId) {
+        super(session, entityId, geyserId, uuid, EntityDefinitions.FALLING_BLOCK, position, motion, yaw, pitch, 0f);
         this.javaId = javaId;
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        this.metadata.put(EntityData.VARIANT, session.getBlockMappings().getBedrockBlockId(javaId));
-        super.spawnEntity(session);
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        this.dirtyMetadata.put(EntityData.VARIANT, session.getBlockMappings().getBedrockBlockId(javaId));
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
+    public void setGravity(EntityMetadata<Boolean> entityMetadata) {
+        super.setGravity(entityMetadata);
         // Set the NO_AI flag based on the no gravity flag to prevent movement
-        if (entityMetadata.getId() == 5) {
-            this.metadata.getFlags().setFlag(EntityFlag.NO_AI, (boolean) entityMetadata.getValue());
-        }
+        setFlag(EntityFlag.NO_AI, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java
index 9de2102d5..fae7ca421 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java
@@ -37,7 +37,6 @@ import com.nukkitx.nbt.NbtType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket;
 import org.geysermc.connector.entity.player.PlayerEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.utils.FireworkColor;
 import org.geysermc.connector.utils.MathUtils;
@@ -46,118 +45,116 @@ import org.geysermc.floodgate.util.DeviceOs;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.OptionalInt;
+import java.util.UUID;
 
 public class FireworkEntity extends Entity {
 
-    public FireworkEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public FireworkEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8) {
-            ItemStack item = (ItemStack) entityMetadata.getValue();
-            if (item == null) {
-                return;
-            }
-            CompoundTag tag = item.getNbt();
+    public void setFireworkItem(EntityMetadata<ItemStack> entityMetadata) {
+        ItemStack item = entityMetadata.getValue();
+        if (item == null) {
+            return;
+        }
+        CompoundTag tag = item.getNbt();
 
-            if (tag == null) {
-                return;
-            }
+        if (tag == null) {
+            return;
+        }
 
-            // TODO: Remove once Mojang fixes bugs with fireworks crashing clients on these specific devices.
-            // https://bugs.mojang.com/browse/MCPE-89115
-            if (session.getClientData().getDeviceOs() == DeviceOs.XBOX
-                    || session.getClientData().getDeviceOs() == DeviceOs.PS4) {
-                return;
-            }
+        // TODO: Remove once Mojang fixes bugs with fireworks crashing clients on these specific devices.
+        // https://bugs.mojang.com/browse/MCPE-89115
+        if (session.getClientData().getDeviceOs() == DeviceOs.XBOX
+                || session.getClientData().getDeviceOs() == DeviceOs.PS4) {
+            return;
+        }
 
-            CompoundTag fireworks = tag.get("Fireworks");
-            if (fireworks == null) {
-                // Thank you Mineplex very cool
-                return;
-            }
+        CompoundTag fireworks = tag.get("Fireworks");
+        if (fireworks == null) {
+            // Thank you Mineplex very cool
+            return;
+        }
 
-            NbtMapBuilder fireworksBuilder = NbtMap.builder();
-            if (fireworks.get("Flight") != null) {
-                fireworksBuilder.putByte("Flight", MathUtils.getNbtByte(fireworks.get("Flight").getValue()));
-            }
+        NbtMapBuilder fireworksBuilder = NbtMap.builder();
+        if (fireworks.get("Flight") != null) {
+            fireworksBuilder.putByte("Flight", MathUtils.getNbtByte(fireworks.get("Flight").getValue()));
+        }
 
-            List<NbtMap> explosions = new ArrayList<>();
-            if (fireworks.get("Explosions") != null) {
-                for (Tag effect : ((ListTag) fireworks.get("Explosions")).getValue()) {
-                    CompoundTag effectData = (CompoundTag) effect;
-                    NbtMapBuilder effectBuilder = NbtMap.builder();
+        List<NbtMap> explosions = new ArrayList<>();
+        if (fireworks.get("Explosions") != null) {
+            for (Tag effect : ((ListTag) fireworks.get("Explosions")).getValue()) {
+                CompoundTag effectData = (CompoundTag) effect;
+                NbtMapBuilder effectBuilder = NbtMap.builder();
 
-                    if (effectData.get("Type") != null) {
-                        effectBuilder.putByte("FireworkType", MathUtils.getNbtByte(effectData.get("Type").getValue()));
-                    }
-
-                    if (effectData.get("Colors") != null) {
-                        int[] oldColors = (int[]) effectData.get("Colors").getValue();
-                        byte[] colors = new byte[oldColors.length];
-
-                        int i = 0;
-                        for (int color : oldColors) {
-                            colors[i++] = FireworkColor.fromJavaRGB(color);
-                        }
-
-                        effectBuilder.putByteArray("FireworkColor", colors);
-                    }
-
-                    if (effectData.get("FadeColors") != null) {
-                        int[] oldColors = (int[]) effectData.get("FadeColors").getValue();
-                        byte[] colors = new byte[oldColors.length];
-
-                        int i = 0;
-                        for (int color : oldColors) {
-                            colors[i++] = FireworkColor.fromJavaRGB(color);
-                        }
-
-                        effectBuilder.putByteArray("FireworkFade", colors);
-                    }
-
-                    if (effectData.get("Trail") != null) {
-                        effectBuilder.putByte("FireworkTrail", MathUtils.getNbtByte(effectData.get("Trail").getValue()));
-                    }
-
-                    if (effectData.get("Flicker") != null) {
-                        effectBuilder.putByte("FireworkFlicker", MathUtils.getNbtByte(effectData.get("Flicker").getValue()));
-                    }
-
-                    explosions.add(effectBuilder.build());
+                if (effectData.get("Type") != null) {
+                    effectBuilder.putByte("FireworkType", MathUtils.getNbtByte(effectData.get("Type").getValue()));
                 }
-            }
 
-            fireworksBuilder.putList("Explosions", NbtType.COMPOUND, explosions);
+                if (effectData.get("Colors") != null) {
+                    int[] oldColors = (int[]) effectData.get("Colors").getValue();
+                    byte[] colors = new byte[oldColors.length];
 
-            NbtMapBuilder builder = NbtMap.builder();
-            builder.put("Fireworks", fireworksBuilder.build());
-            metadata.put(EntityData.DISPLAY_ITEM, builder.build());
-        } else if (entityMetadata.getId() == 9) {
-            OptionalInt optional = (OptionalInt) entityMetadata.getValue();
-            // Checks if the firework has an entity ID (used when a player is gliding)
-            // 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.getRotation().getX();
-                float pitch = entity.getRotation().getY();
-                // 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());
+                    int i = 0;
+                    for (int color : oldColors) {
+                        colors[i++] = FireworkColor.fromJavaRGB(color);
+                    }
 
-                session.sendUpstreamPacket(entityMotionPacket);
+                    effectBuilder.putByteArray("FireworkColor", colors);
+                }
+
+                if (effectData.get("FadeColors") != null) {
+                    int[] oldColors = (int[]) effectData.get("FadeColors").getValue();
+                    byte[] colors = new byte[oldColors.length];
+
+                    int i = 0;
+                    for (int color : oldColors) {
+                        colors[i++] = FireworkColor.fromJavaRGB(color);
+                    }
+
+                    effectBuilder.putByteArray("FireworkFade", colors);
+                }
+
+                if (effectData.get("Trail") != null) {
+                    effectBuilder.putByte("FireworkTrail", MathUtils.getNbtByte(effectData.get("Trail").getValue()));
+                }
+
+                if (effectData.get("Flicker") != null) {
+                    effectBuilder.putByte("FireworkFlicker", MathUtils.getNbtByte(effectData.get("Flicker").getValue()));
+                }
+
+                explosions.add(effectBuilder.build());
             }
         }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+        fireworksBuilder.putList("Explosions", NbtType.COMPOUND, explosions);
+
+        NbtMapBuilder builder = NbtMap.builder();
+        builder.put("Fireworks", fireworksBuilder.build());
+        dirtyMetadata.put(EntityData.DISPLAY_ITEM, builder.build());
+    }
+
+    public void setPlayerGliding(EntityMetadata<OptionalInt> entityMetadata) {
+        OptionalInt optional = entityMetadata.getValue();
+        // Checks if the firework has an entity ID (used when a player is gliding)
+        // 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);
+        }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java
index 565614143..2493ef588 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java
@@ -26,12 +26,12 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket;
 import org.geysermc.connector.entity.player.PlayerEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.collision.BoundingBox;
 import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
@@ -40,6 +40,7 @@ import org.geysermc.connector.registry.BlockRegistries;
 import org.geysermc.connector.utils.BlockPositionIterator;
 import org.geysermc.connector.utils.BlockUtils;
 
+import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 
 public class FishingHookEntity extends ThrowableEntity {
@@ -50,41 +51,44 @@ public class FishingHookEntity extends ThrowableEntity {
 
     private boolean inWater = false;
 
-    public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, PlayerEntity owner) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public FishingHookEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, Vector3f position, Vector3f motion, float yaw, float pitch, PlayerEntity owner) {
+        super(session, entityId, geyserId, uuid, EntityDefinitions.FISHING_BOBBER, position, motion, yaw, pitch, 0f);
 
         this.boundingBox = new BoundingBox(0.125, 0.125, 0.125, 0.25, 0.25, 0.25);
 
         // In Java, the splash sound depends on the entity's velocity, but in Bedrock the volume doesn't change.
         // This splash can be confused with the sound from catching a fish. This silences the splash from Bedrock,
         // so that it can be handled by moveAbsoluteImmediate.
-        this.metadata.putFloat(EntityData.BOUNDING_BOX_HEIGHT, 128);
+        this.dirtyMetadata.putFloat(EntityData.BOUNDING_BOX_HEIGHT, 128);
 
-        this.metadata.put(EntityData.OWNER_EID, owner.getGeyserId());
+        this.dirtyMetadata.put(EntityData.OWNER_EID, owner.getGeyserId());
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8) { // Hooked entity
-            int hookedEntityId = (int) entityMetadata.getValue() - 1;
-            Entity entity = session.getEntityCache().getEntityByJavaId(hookedEntityId);
-            if (entity == null && session.getPlayerEntity().getEntityId() == hookedEntityId) {
-                entity = session.getPlayerEntity();
-            }
+    public void spawnEntity() {
 
-            if (entity != null) {
-                metadata.put(EntityData.TARGET_EID, entity.getGeyserId());
-                hooked = true;
-            } else {
-                hooked = false;
-            }
+        super.spawnEntity();
+    }
+
+    public void setHookedEntity(EntityMetadata<Integer> entityMetadata) {
+        int hookedEntityId = ((IntEntityMetadata) entityMetadata).getPrimitiveValue() - 1;
+        Entity entity;
+        if (session.getPlayerEntity().getEntityId() == hookedEntityId) {
+            entity = session.getPlayerEntity();
+        } else {
+            entity = session.getEntityCache().getEntityByJavaId(hookedEntityId);
         }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+        if (entity != null) {
+            dirtyMetadata.put(EntityData.TARGET_EID, entity.getGeyserId());
+            hooked = true;
+        } else {
+            hooked = false;
+        }
     }
 
     @Override
-    protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
+    protected void moveAbsoluteImmediate(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
         boundingBox.setMiddleX(position.getX());
         boundingBox.setMiddleY(position.getY() + boundingBox.getSizeY() / 2);
         boundingBox.setMiddleZ(position.getZ());
@@ -123,14 +127,14 @@ public class FishingHookEntity extends ThrowableEntity {
         inWater = touchingWater;
 
         if (!collided) {
-            super.moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported);
+            super.moveAbsoluteImmediate(position, yaw, pitch, headYaw, isOnGround, teleported);
         } else {
-            super.moveAbsoluteImmediate(session, this.position, rotation, true, true);
+            super.moveAbsoluteImmediate(this.position, yaw, pitch, headYaw, true, true);
         }
     }
 
     private void sendSplashSound(GeyserSession session) {
-        if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) {
+        if (!getFlag(EntityFlag.SILENT)) {
             float volume = (float) (0.2f * Math.sqrt(0.2 * (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) + motion.getY() * motion.getY()));
             if (volume > 1) {
                 volume = 1;
@@ -145,39 +149,38 @@ public class FishingHookEntity extends ThrowableEntity {
     }
 
     @Override
-    public void tick(GeyserSession session) {
-        if (hooked || !isInAir(session) && !isInWater(session) || isOnGround()) {
+    public void tick() {
+        if (hooked || !isInAir() && !isInWater() || isOnGround()) {
             motion = Vector3f.ZERO;
             return;
         }
-        float gravity = getGravity(session);
+        float gravity = getGravity();
         motion = motion.down(gravity);
 
-        moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false);
+        moveAbsoluteImmediate(position.add(motion), yaw, pitch, headYaw, onGround, false);
 
-        float drag = getDrag(session);
+        float drag = getDrag();
         motion = motion.mul(drag);
     }
 
     @Override
-    protected float getGravity(GeyserSession session) {
-        if (!isInWater(session) && !onGround) {
+    protected float getGravity() {
+        if (!isInWater() && !onGround) {
             return 0.03f;
         }
         return 0;
     }
 
     /**
-     * @param session the session of the Bedrock client.
      * @return true if this entity is currently in air.
      */
-    protected boolean isInAir(GeyserSession session) {
+    protected boolean isInAir() {
         int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
         return block == BlockStateValues.JAVA_AIR_ID;
     }
 
     @Override
-    protected float getDrag(GeyserSession session) {
+    protected float getDrag() {
         return 0.92f;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java
index fea1e62bd..d6791a15b 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java
@@ -26,33 +26,29 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.world.block.BlockStateValues;
 
-public class FurnaceMinecartEntity extends DefaultBlockMinecartEntity {
+import java.util.UUID;
 
+public class FurnaceMinecartEntity extends DefaultBlockMinecartEntity {
     private boolean hasFuel = false;
 
-    public FurnaceMinecartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public FurnaceMinecartEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setHasFuel(EntityMetadata<Boolean> entityMetadata) {
+        hasFuel = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        updateDefaultBlockMetadata();
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 14 && !showCustomBlock) {
-            hasFuel = (boolean) entityMetadata.getValue();
-            updateDefaultBlockMetadata(session);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    public void updateDefaultBlockMetadata(GeyserSession session) {
-        metadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(hasFuel ? BlockStateValues.JAVA_FURNACE_LIT_ID : BlockStateValues.JAVA_FURNACE_ID));
-        metadata.put(EntityData.DISPLAY_OFFSET, 6);
+    public void updateDefaultBlockMetadata() {
+        dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(hasFuel ? BlockStateValues.JAVA_FURNACE_LIT_ID : BlockStateValues.JAVA_FURNACE_ID));
+        dirtyMetadata.put(EntityData.DISPLAY_OFFSET, 6);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemEntity.java
index ba354c525..ab5e2906e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/ItemEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/ItemEntity.java
@@ -34,23 +34,24 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
 import com.nukkitx.protocol.bedrock.packet.AddItemEntityPacket;
 import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.item.ItemTranslator;
 import org.geysermc.connector.network.translators.world.block.BlockStateValues;
 
+import java.util.UUID;
+
 public class ItemEntity extends ThrowableEntity {
 
     protected ItemData item;
 
     private int waterLevel = -1;
 
-    public ItemEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ItemEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
+    public void spawnEntity() {
         if (item == null) {
             return;
         }
@@ -58,36 +59,59 @@ public class ItemEntity extends ThrowableEntity {
         AddItemEntityPacket itemPacket = new AddItemEntityPacket();
         itemPacket.setRuntimeEntityId(geyserId);
         itemPacket.setUniqueEntityId(geyserId);
-        itemPacket.setPosition(position.add(0d, this.entityType.getOffset(), 0d));
+        itemPacket.setPosition(position.add(0d, this.definition.offset(), 0d));
         itemPacket.setMotion(motion);
         itemPacket.setFromFishing(false);
         itemPacket.setItemInHand(item);
-        itemPacket.getMetadata().putAll(metadata);
+        itemPacket.getMetadata().putAll(dirtyMetadata);
         session.sendUpstreamPacket(itemPacket);
     }
 
     @Override
-    public void tick(GeyserSession session) {
-        if (isInWater(session)) {
+    public void tick() {
+        if (isInWater()) {
             return;
         }
         if (!onGround || (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) > 0.00001) {
-            float gravity = getGravity(session);
+            float gravity = getGravity();
             motion = motion.down(gravity);
-            moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false);
-            float drag = getDrag(session);
+            moveAbsoluteImmediate(position.add(motion), yaw, pitch, headYaw, onGround, false);
+            float drag = getDrag();
             motion = motion.mul(drag, 0.98f, drag);
         }
     }
 
+    public void setItem(EntityMetadata<ItemStack> entityMetadata) {
+        ItemData item = ItemTranslator.translateToBedrock(session, entityMetadata.getValue());
+        if (this.item == null) {
+            this.item = item;
+            spawnEntity();
+        } else if (item.equals(this.item, false, true, true)) {
+            // Don't bother respawning the entity if items are equal
+            if (this.item.getCount() != item.getCount()) {
+                // Just item count updated; let's make this easy
+                this.item = item;
+                EntityEventPacket packet = new EntityEventPacket();
+                packet.setRuntimeEntityId(geyserId);
+                packet.setType(EntityEventType.UPDATE_ITEM_STACK_SIZE);
+                packet.setData(this.item.getCount());
+                session.sendUpstreamPacket(packet);
+            }
+        } else {
+            this.item = item;
+            despawnEntity();
+            spawnEntity();
+        }
+    }
+
     @Override
-    protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
-        float offset = entityType.getOffset();
+    protected void moveAbsoluteImmediate(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
+        float offset = definition.offset();
         if (waterLevel == 0) { // Item is in a full block of water
             // Move the item entity down so it doesn't float above the water
-            offset = -entityType.getOffset();
+            offset = -definition.offset();
         }
-        super.moveAbsoluteImmediate(session, position.add(0, offset, 0), Vector3f.ZERO, isOnGround, teleported);
+        super.moveAbsoluteImmediate(position.add(0, offset, 0), 0, 0, 0, isOnGround, teleported);
         this.position = position;
 
         int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
@@ -95,8 +119,8 @@ public class ItemEntity extends ThrowableEntity {
     }
 
     @Override
-    protected float getGravity(GeyserSession session) {
-        if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY) && !onGround && !isInWater(session)) {
+    protected float getGravity() {
+        if (getFlag(EntityFlag.HAS_GRAVITY) && !onGround && !isInWater()) {
             // Gravity can change if the item is in water/lava, but
             // the server calculates the motion & position for us
             return 0.04f;
@@ -105,7 +129,7 @@ public class ItemEntity extends ThrowableEntity {
     }
 
     @Override
-    protected float getDrag(GeyserSession session) {
+    protected float getDrag() {
         if (onGround) {
             Vector3i groundBlockPos = position.toInt().down(1);
             int blockState = session.getConnector().getWorldManager().getBlockAt(session, groundBlockPos);
@@ -115,35 +139,7 @@ public class ItemEntity extends ThrowableEntity {
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8) {
-            ItemData item = ItemTranslator.translateToBedrock(session, (ItemStack) entityMetadata.getValue());
-            if (this.item == null) {
-                this.item = item;
-                spawnEntity(session);
-            } else if (item.equals(this.item, false, true, true)) {
-                // Don't bother respawning the entity if items are equal
-                if (this.item.getCount() != item.getCount()) {
-                    // Just item count updated; let's make this easy
-                    this.item = item;
-                    EntityEventPacket packet = new EntityEventPacket();
-                    packet.setRuntimeEntityId(geyserId);
-                    packet.setType(EntityEventType.UPDATE_ITEM_STACK_SIZE);
-                    packet.setData(this.item.getCount());
-                    session.sendUpstreamPacket(packet);
-                }
-            } else {
-                this.item = item;
-                despawnEntity(session);
-                spawnEntity(session);
-            }
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    protected boolean isInWater(GeyserSession session) {
+    protected boolean isInWater() {
         return waterLevel != -1;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java
index 6411a4f55..5730fdaff 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java
@@ -27,7 +27,9 @@ package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
-import com.github.steveice10.mc.protocol.data.game.entity.object.HangingDirection;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.nbt.NbtMap;
@@ -37,29 +39,25 @@ import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
 import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
 import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
 import lombok.Getter;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.item.ItemTranslator;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 /**
  * Item frames are an entity in Java but a block entity in Bedrock.
  */
 public class ItemFrameEntity extends Entity {
-
-    /**
-     * Used to construct the block entity tag on spawning.
-     */
-    private final HangingDirection direction;
     /**
      * Used for getting the Bedrock block position.
      * Blocks deal with integers whereas entities deal with floats.
      */
-    private Vector3i bedrockPosition;
+    private final Vector3i bedrockPosition;
     /**
      * Specific block 'state' we are emulating in Bedrock.
      */
-    private int bedrockRuntimeId;
+    private final int bedrockRuntimeId;
     /**
      * Rotation of item in frame.
      */
@@ -73,16 +71,16 @@ public class ItemFrameEntity extends Entity {
      */
     @Getter
     private ItemStack heldItem = null;
+    /**
+     * Determines if this entity needs updated on the client end/
+     */
+    private boolean changed = true;
 
-    public ItemFrameEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, HangingDirection direction) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-        this.direction = direction;
-    }
+    public ItemFrameEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, Direction direction) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, 0f);
 
-    @Override
-    public void spawnEntity(GeyserSession session) {
         NbtMapBuilder blockBuilder = NbtMap.builder()
-                .putString("name", this.entityType == EntityType.GLOW_ITEM_FRAME ? "minecraft:glow_frame" : "minecraft:frame")
+                .putString("name", this.definition.entityType() == EntityType.GLOW_ITEM_FRAME ? "minecraft:glow_frame" : "minecraft:frame")
                 .putInt("version", session.getBlockMappings().getBlockStateVersion());
         NbtMapBuilder statesBuilder = NbtMap.builder()
                 .putInt("facing_direction", direction.ordinal())
@@ -96,18 +94,26 @@ public class ItemFrameEntity extends Entity {
         bedrockPosition = Vector3i.from(position.getFloorX(), position.getFloorY(), position.getFloorZ());
 
         session.getItemFrameCache().put(bedrockPosition, this);
+    }
 
-        updateBlock(session);
+    @Override
+    protected void initializeMetadata() {
+        // lol nah don't do anything
+        // This isn't a real entity for Bedrock so it isn't going to do anything
+    }
+
+    @Override
+    public void spawnEntity() {
+        updateBlock();
         session.getConnector().getLogger().debug("Spawned item frame at location " + bedrockPosition + " with java id " + entityId);
         valid = true;
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8 && entityMetadata.getValue() != null) {
-            this.heldItem = (ItemStack) entityMetadata.getValue();
+    public void setItemInFrame(EntityMetadata<ItemStack> entityMetadata) {
+        if (entityMetadata.getValue() != null) {
+            this.heldItem = entityMetadata.getValue();
             ItemData itemData = ItemTranslator.translateToBedrock(session, heldItem);
-            ItemMapping mapping = session.getItemMappings().getMapping((ItemStack) entityMetadata.getValue());
+            ItemMapping mapping = session.getItemMappings().getMapping(entityMetadata.getValue());
             NbtMapBuilder builder = NbtMap.builder();
 
             builder.putByte("Count", (byte) itemData.getCount());
@@ -121,34 +127,30 @@ public class ItemFrameEntity extends Entity {
             tag.putFloat("ItemDropChance", 1.0f);
             tag.putFloat("ItemRotation", rotation);
             cachedTag = tag.build();
-            updateBlock(session);
-        }
-        else if (entityMetadata.getId() == 8 && entityMetadata.getValue() == null && cachedTag != null) {
+            changed = true;
+        } else if (cachedTag != null) {
             cachedTag = getDefaultTag();
-            updateBlock(session);
-        }
-        else if (entityMetadata.getId() == 9) {
-            rotation = ((int) entityMetadata.getValue()) * 45;
-            if (cachedTag == null) {
-                updateBlock(session);
-                return;
-            }
-            NbtMapBuilder builder = cachedTag.toBuilder();
-            builder.putFloat("ItemRotation", rotation);
-            cachedTag = builder.build();
-            updateBlock(session);
-        }
-        else {
-            updateBlock(session);
+            changed = true;
         }
     }
 
+    public void setItemRotation(EntityMetadata<Integer> entityMetadata) {
+        rotation = ((IntEntityMetadata) entityMetadata).getPrimitiveValue() * 45;
+        if (cachedTag == null) {
+            return;
+        }
+        NbtMapBuilder builder = cachedTag.toBuilder();
+        builder.putFloat("ItemRotation", rotation);
+        cachedTag = builder.build();
+        changed = true;
+    }
+
     @Override
-    public boolean despawnEntity(GeyserSession session) {
+    public boolean despawnEntity() {
         UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
         updateBlockPacket.setDataLayer(0);
         updateBlockPacket.setBlockPosition(bedrockPosition);
-        updateBlockPacket.setRuntimeId(session.getBlockMappings().getBedrockAirId());
+        updateBlockPacket.setRuntimeId(session.getBlockMappings().getBedrockAirId()); //TODO maybe set this to the world block or another item frame?
         updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.PRIORITY);
         updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK);
         updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS);
@@ -166,15 +168,23 @@ public class ItemFrameEntity extends Entity {
         builder.putInt("y", bedrockPosition.getY());
         builder.putInt("z", bedrockPosition.getZ());
         builder.putByte("isMovable", (byte) 1);
-        builder.putString("id", this.entityType == EntityType.GLOW_ITEM_FRAME ? "GlowItemFrame" : "ItemFrame");
+        builder.putString("id", this.definition.entityType() == EntityType.GLOW_ITEM_FRAME ? "GlowItemFrame" : "ItemFrame");
         return builder.build();
     }
 
+    @Override
+    public void updateBedrockMetadata() {
+        updateBlock();
+    }
+
     /**
      * Updates the item frame as a block
-     * @param session GeyserSession.
      */
-    public void updateBlock(GeyserSession session) {
+    public void updateBlock() {
+        if (!changed) {
+            // Don't send a block update packet - nothing changed
+            return;
+        }
         UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
         updateBlockPacket.setDataLayer(0);
         updateBlockPacket.setBlockPosition(bedrockPosition);
@@ -193,6 +203,8 @@ public class ItemFrameEntity extends Entity {
         }
 
         session.sendUpstreamPacket(blockEntityDataPacket);
+
+        changed = false;
     }
 
     /**
diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java
index 58a3b6f6c..16f361683 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java
@@ -26,9 +26,10 @@
 package org.geysermc.connector.entity;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class ItemedFireballEntity extends ThrowableEntity {
     private final Vector3f acceleration;
 
@@ -37,8 +38,8 @@ public class ItemedFireballEntity extends ThrowableEntity {
      */
     protected int futureTicks = 3;
 
-    public ItemedFireballEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, Vector3f.ZERO, rotation);
+    public ItemedFireballEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, Vector3f.ZERO, yaw, pitch, headYaw);
 
         float magnitude = motion.length();
         if (magnitude != 0) {
@@ -48,28 +49,28 @@ public class ItemedFireballEntity extends ThrowableEntity {
         }
     }
 
-    private Vector3f tickMovement(GeyserSession session, Vector3f position) {
+    private Vector3f tickMovement(Vector3f position) {
         position = position.add(motion);
-        float drag = getDrag(session);
+        float drag = getDrag();
         motion = motion.add(acceleration).mul(drag);
         return position;
     }
 
     @Override
-    protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
+    protected void moveAbsoluteImmediate(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
         // Advance the position by a few ticks before sending it to Bedrock
         Vector3f lastMotion = motion;
         Vector3f newPosition = position;
         for (int i = 0; i < futureTicks; i++) {
-            newPosition = tickMovement(session, newPosition);
+            newPosition = tickMovement(newPosition);
         }
-        super.moveAbsoluteImmediate(session, newPosition, rotation, isOnGround, teleported);
+        super.moveAbsoluteImmediate(newPosition, yaw, pitch, headYaw, isOnGround, teleported);
         this.position = position;
         this.motion = lastMotion;
     }
 
     @Override
-    public void tick(GeyserSession session) {
-        moveAbsoluteImmediate(session, tickMovement(session, position), rotation, false, false);
+    public void tick() {
+        moveAbsoluteImmediate(tickMovement(position), yaw, pitch, headYaw, false, false);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/LeashKnotEntity.java b/connector/src/main/java/org/geysermc/connector/entity/LeashKnotEntity.java
index a10a30a8d..c5697346e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/LeashKnotEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/LeashKnotEntity.java
@@ -26,13 +26,15 @@
 package org.geysermc.connector.entity;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class LeashKnotEntity extends Entity {
 
-    public LeashKnotEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
+    public LeashKnotEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
         // Position is incorrect by default
-        super(entityId, geyserId, entityType, position.add(0.5f, 0.25f, 0.5f), motion, rotation);
+        super(session, entityId, geyserId, uuid, definition, position.add(0.5f, 0.25f, 0.5f), motion, yaw, pitch, headYaw);
     }
 
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/LightningEntity.java b/connector/src/main/java/org/geysermc/connector/entity/LightningEntity.java
index 2255519e5..d9c5655b3 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/LightningEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/LightningEntity.java
@@ -27,20 +27,20 @@ package org.geysermc.connector.entity;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 
 public class LightningEntity extends Entity {
 
-    public LightningEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public LightningEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        super.spawnEntity(session);
+    public void spawnEntity() {
+        super.spawnEntity();
 
         // Add these two sound effects - they're done completely clientside on Java Edition as of 1.17.1
         ThreadLocalRandom random = ThreadLocalRandom.current();
diff --git a/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java b/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java
index 44a9859d5..28f97a2a3 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java
@@ -29,6 +29,8 @@ import com.github.steveice10.mc.protocol.data.game.entity.attribute.Attribute;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.data.AttributeData;
@@ -43,7 +45,6 @@ import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Setter;
 import org.geysermc.connector.entity.attribute.GeyserAttributeType;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 import org.geysermc.connector.utils.AttributeUtils;
@@ -52,6 +53,7 @@ import org.geysermc.connector.utils.ChunkUtils;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.UUID;
 
 @Getter
 @Setter
@@ -74,82 +76,87 @@ public class LivingEntity extends Entity {
      */
     private boolean isMaxFrozenState = false;
 
-    public LivingEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public LivingEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
 
+    @Override
+    protected void initializeMetadata() {
+        super.initializeMetadata();
         // Matches Bedrock behavior; is always set to this
-        metadata.put(EntityData.HEALTH, 1);
+        dirtyMetadata.put(EntityData.HEALTH, 1);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        switch (entityMetadata.getId()) {
-            case 8 -> { // blocking
-                byte xd = (byte) entityMetadata.getValue();
+    public void setLivingEntityFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
 
-                //blocking gets triggered when using a bow, but if we set USING_ITEM for all items, it may look like
-                //you're "mining" with ex. a shield.
-                ItemMapping shield = session.getItemMappings().getStoredItems().shield();
-                boolean isUsingShield = (getHand().getId() == shield.getBedrockId() ||
-                        getHand().equals(ItemData.AIR) && getOffHand().getId() == shield.getBedrockId());
-                metadata.getFlags().setFlag(EntityFlag.USING_ITEM, (xd & 0x01) == 0x01 && !isUsingShield);
-                metadata.getFlags().setFlag(EntityFlag.BLOCKING, (xd & 0x01) == 0x01);
+        // Blocking gets triggered when using a bow, but if we set USING_ITEM for all items, it may look like
+        // you're "mining" with ex. a shield.
+        ItemMapping shield = session.getItemMappings().getStoredItems().shield();
+        boolean isUsingShield = (getHand().getId() == shield.getBedrockId() ||
+                getHand().equals(ItemData.AIR) && getOffHand().getId() == shield.getBedrockId());
+        setFlag(EntityFlag.USING_ITEM, (xd & 0x01) == 0x01 && !isUsingShield);
+        setFlag(EntityFlag.BLOCKING, (xd & 0x01) == 0x01);
 
-                // Riptide spin attack
-                metadata.getFlags().setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, (xd & 0x04) == 0x04);
-            }
-            case 9 -> {
-                this.health = (float) entityMetadata.getValue();
+        // Riptide spin attack
+        setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, (xd & 0x04) == 0x04);
+    }
 
-                AttributeData healthData = createHealthAttribute();
-                UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
-                attributesPacket.setRuntimeEntityId(geyserId);
-                attributesPacket.setAttributes(Collections.singletonList(healthData));
-                session.sendUpstreamPacket(attributesPacket);
-            }
-            case 10 -> metadata.put(EntityData.EFFECT_COLOR, entityMetadata.getValue());
-            case 11 -> metadata.put(EntityData.EFFECT_AMBIENT, (byte) ((boolean) entityMetadata.getValue() ? 1 : 0));
-            case 14 -> { // Bed Position
-                Position bedPosition = (Position) entityMetadata.getValue();
-                if (bedPosition != null) {
-                    metadata.put(EntityData.BED_POSITION, Vector3i.from(bedPosition.getX(), bedPosition.getY(), bedPosition.getZ()));
-                    int bed = session.getConnector().getWorldManager().getBlockAt(session, bedPosition);
-                    // Bed has to be updated, or else player is floating in the air
-                    ChunkUtils.updateBlock(session, bed, bedPosition);
-                    // Indicate that the player should enter the sleep cycle
-                    // Has to be a byte or it does not work
-                    // (Bed position is what actually triggers sleep - "pose" is only optional)
-                    metadata.put(EntityData.PLAYER_FLAGS, (byte) 2);
-                } else {
-                    // Player is no longer sleeping
-                    metadata.put(EntityData.PLAYER_FLAGS, (byte) 0);
-                }
-            }
+    public void setHealth(EntityMetadata<Float> entityMetadata) {
+        this.health = ((FloatEntityMetadata) entityMetadata).getPrimitiveValue();
+
+        AttributeData healthData = createHealthAttribute();
+        UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
+        attributesPacket.setRuntimeEntityId(geyserId);
+        attributesPacket.setAttributes(Collections.singletonList(healthData));
+        session.sendUpstreamPacket(attributesPacket);
+    }
+
+    public Vector3i setBedPosition(EntityMetadata<Position> entityMetadata) {
+        Position bedPosition = entityMetadata.getValue();
+        if (bedPosition != null) {
+            Vector3i vector = Vector3i.from(bedPosition.getX(), bedPosition.getY(), bedPosition.getZ());
+            dirtyMetadata.put(EntityData.BED_POSITION, vector);
+            int bed = session.getConnector().getWorldManager().getBlockAt(session, bedPosition);
+            // Bed has to be updated, or else player is floating in the air
+            ChunkUtils.updateBlock(session, bed, bedPosition);
+            // Indicate that the player should enter the sleep cycle
+            // Has to be a byte or it does not work
+            // (Bed position is what actually triggers sleep - "pose" is only optional)
+            dirtyMetadata.put(EntityData.PLAYER_FLAGS, (byte) 2);
+            return vector;
+        } else {
+            // Player is no longer sleeping
+            dirtyMetadata.put(EntityData.PLAYER_FLAGS, (byte) 0);
+            return null;
         }
-
-        super.updateBedrockMetadata(entityMetadata, session);
     }
 
     @Override
-    protected boolean isShaking(GeyserSession session) {
+    protected boolean isShaking() {
         return isMaxFrozenState;
     }
 
     @Override
     protected void setDimensions(Pose pose) {
         if (pose == Pose.SLEEPING) {
-            metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.2f);
-            metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.2f);
+            boundingBoxWidth = 0.2f;
+            boundingBoxHeight = 0.2f;
+            if (boundingBoxWidth != definition.width() || boundingBoxHeight != definition.height()) {
+                dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, boundingBoxWidth);
+                dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, boundingBoxHeight);
+            }
         } else {
             super.setDimensions(pose);
         }
     }
 
     @Override
-    protected void setFreezing(GeyserSession session, float amount) {
-        super.setFreezing(session, amount);
-        this.isMaxFrozenState = amount >= 1.0f;
-        metadata.getFlags().setFlag(EntityFlag.SHAKING, isShaking(session));
+    public float setFreezing(EntityMetadata<Integer> entityMetadata) {
+        float freezingPercentage = super.setFreezing(entityMetadata);
+        this.isMaxFrozenState = freezingPercentage >= 1.0f;
+        setFlag(EntityFlag.SHAKING, isShaking());
+        return freezingPercentage;
     }
 
     /**
diff --git a/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java
index 6ab9a1b22..5b514310d 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java
@@ -26,63 +26,42 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class MinecartEntity extends Entity {
 
-    public MinecartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position.add(0d, entityType.getOffset(), 0d), motion, rotation);
+    public MinecartEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position.add(0d, definition.offset(), 0d), motion, yaw, pitch, headYaw);
+    }
+
+    public void setCustomBlock(EntityMetadata<Integer> entityMetadata) {
+        dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(((IntEntityMetadata) entityMetadata).getPrimitiveValue()));
+    }
+
+    public void setCustomBlockOffset(EntityMetadata<Integer> entityMetadata) {
+        dirtyMetadata.put(EntityData.DISPLAY_OFFSET, entityMetadata.getValue());
+    }
+
+    public void setShowCustomBlock(EntityMetadata<Boolean> entityMetadata) {
+        // If the custom block should be enabled
+        // Needs a byte based off of Java's boolean
+        dirtyMetadata.put(EntityData.CUSTOM_DISPLAY, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0));
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-
-        if (entityMetadata.getId() == 8) {
-            metadata.put(EntityData.HEALTH, entityMetadata.getValue());
-        }
-
-        // Direction in which the minecart is shaking
-        if (entityMetadata.getId() == 9) {
-            metadata.put(EntityData.HURT_DIRECTION, entityMetadata.getValue());
-        }
-
-        // Power in Java, time in Bedrock
-        if (entityMetadata.getId() == 10) {
-            metadata.put(EntityData.HURT_TIME, Math.min((int) (float) entityMetadata.getValue(), 15));
-        }
-
-        if (!(this instanceof DefaultBlockMinecartEntity)) { // Handled in the DefaultBlockMinecartEntity class
-            // Custom block
-            if (entityMetadata.getId() == 11) {
-                metadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId((int) entityMetadata.getValue()));
-            }
-
-            // Custom block offset
-            if (entityMetadata.getId() == 12) {
-                metadata.put(EntityData.DISPLAY_OFFSET, entityMetadata.getValue());
-            }
-
-            // If the custom block should be enabled
-            if (entityMetadata.getId() == 13) {
-                // Needs a byte based off of Java's boolean
-                metadata.put(EntityData.CUSTOM_DISPLAY, (byte) ((boolean) entityMetadata.getValue() ? 1 : 0));
-            }
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
-        super.moveAbsolute(session, position.add(0d, this.entityType.getOffset(), 0d), rotation, isOnGround, teleported);
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
+        super.moveAbsolute(position.add(0d, this.definition.offset(), 0d), yaw, pitch, headYaw, isOnGround, teleported);
     }
 
     @Override
     public Vector3f getBedrockRotation() {
         // Note: minecart rotation on rails does not care about the actual rotation value
-        return Vector3f.from(0, rotation.getX(), 0);
+        return Vector3f.from(0, yaw, 0);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/PaintingEntity.java b/connector/src/main/java/org/geysermc/connector/entity/PaintingEntity.java
index b620e598b..e164883c5 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/PaintingEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/PaintingEntity.java
@@ -27,23 +27,24 @@ package org.geysermc.connector.entity;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.packet.AddPaintingPacket;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.utils.PaintingType;
 
+import java.util.UUID;
+
 public class PaintingEntity extends Entity {
     private static final double OFFSET = -0.46875;
     private final PaintingType paintingName;
     private final int direction;
 
-    public PaintingEntity(long entityId, long geyserId, Vector3f position, PaintingType paintingName, int direction) {
-        super(entityId, geyserId, EntityType.PAINTING, position, Vector3f.ZERO, Vector3f.ZERO);
+    public PaintingEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, Vector3f position, PaintingType paintingName, int direction) {
+        super(session, entityId, geyserId, uuid, EntityDefinitions.PAINTING, position, Vector3f.ZERO, 0f, 0f, 0f);
         this.paintingName = paintingName;
         this.direction = direction;
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
+    public void spawnEntity() {
         AddPaintingPacket addPaintingPacket = new AddPaintingPacket();
         addPaintingPacket.setUniqueEntityId(geyserId);
         addPaintingPacket.setRuntimeEntityId(geyserId);
@@ -58,7 +59,7 @@ public class PaintingEntity extends Entity {
     }
 
     @Override
-    public void updateHeadLookRotation(GeyserSession session, float headYaw) {
+    public void updateHeadLookRotation(float headYaw) {
         // Do nothing, as head look messes up paintings
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java
index c6d4c3aa5..0cf159f0e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java
@@ -27,19 +27,20 @@ package org.geysermc.connector.entity;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.world.block.BlockStateValues;
 
+import java.util.UUID;
+
 public class SpawnerMinecartEntity extends DefaultBlockMinecartEntity {
 
-    public SpawnerMinecartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public SpawnerMinecartEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateDefaultBlockMetadata(GeyserSession session) {
-        metadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(BlockStateValues.JAVA_SPAWNER_ID));
-        metadata.put(EntityData.DISPLAY_OFFSET, 6);
+    public void updateDefaultBlockMetadata() {
+        dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(BlockStateValues.JAVA_SPAWNER_ID));
+        dirtyMetadata.put(EntityData.DISPLAY_OFFSET, 6);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/TNTEntity.java b/connector/src/main/java/org/geysermc/connector/entity/TNTEntity.java
index 08ffa0dbc..39482b16e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/TNTEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/TNTEntity.java
@@ -26,41 +26,37 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
-public class TNTEntity extends Entity implements Tickable {
+import java.util.UUID;
 
+public class TNTEntity extends Entity implements Tickable {
     private int currentTick;
 
-    public TNTEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public TNTEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setFuseLength(EntityMetadata<Integer> entityMetadata) {
+        currentTick = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.IGNITED, true);
+        dirtyMetadata.put(EntityData.FUSE_LENGTH, currentTick);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8) {
-            currentTick = (int) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.IGNITED, true);
-            metadata.put(EntityData.FUSE_LENGTH, currentTick);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    public void tick(GeyserSession session) {
+    public void tick() {
         if (currentTick == 0) {
             // No need to update the fuse when there is none
             return;
         }
 
         if (currentTick % 5 == 0) {
-            metadata.put(EntityData.FUSE_LENGTH, currentTick);
+            dirtyMetadata.put(EntityData.FUSE_LENGTH, currentTick);
 
             SetEntityDataPacket packet = new SetEntityDataPacket();
             packet.setRuntimeEntityId(geyserId);
diff --git a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java
index 1ba18bfd7..5c181fe0f 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java
@@ -25,15 +25,17 @@
 
 package org.geysermc.connector.entity;
 
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.LevelEventType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
 import com.nukkitx.protocol.bedrock.packet.MoveEntityDeltaPacket;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.world.block.BlockStateValues;
 
+import java.util.UUID;
+
 /**
  * Used as a class for any object-like entity that moves as a projectile
  */
@@ -41,8 +43,8 @@ public class ThrowableEntity extends Entity implements Tickable {
 
     protected Vector3f lastJavaPosition;
 
-    public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ThrowableEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
         this.lastJavaPosition = position;
     }
 
@@ -52,14 +54,14 @@ public class ThrowableEntity extends Entity implements Tickable {
      * Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions
      */
     @Override
-    public void tick(GeyserSession session) {
-        moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false);
-        float drag = getDrag(session);
-        float gravity = getGravity(session);
+    public void tick() {
+        moveAbsoluteImmediate(position.add(motion), yaw, pitch, headYaw, onGround, false);
+        float drag = getDrag();
+        float gravity = getGravity();
         motion = motion.mul(drag).down(gravity);
     }
 
-    protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
+    protected void moveAbsoluteImmediate(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
         MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
         moveEntityDeltaPacket.setRuntimeEntityId(geyserId);
 
@@ -86,19 +88,21 @@ public class ThrowableEntity extends Entity implements Tickable {
         }
         setPosition(position);
 
-        if (this.rotation.getX() != rotation.getX()) {
+        if (this.yaw != yaw) {
             moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW);
-            moveEntityDeltaPacket.setYaw(rotation.getX());
+            moveEntityDeltaPacket.setYaw(yaw);
+            this.yaw = yaw;
         }
-        if (this.rotation.getY() != rotation.getY()) {
+        if (this.pitch != pitch) {
             moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);
-            moveEntityDeltaPacket.setPitch(rotation.getY());
+            moveEntityDeltaPacket.setPitch(pitch);
+            this.pitch = pitch;
         }
-        if (this.rotation.getZ() != rotation.getZ()) {
+        if (this.headYaw != headYaw) {
             moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW);
-            moveEntityDeltaPacket.setHeadYaw(rotation.getZ());
+            moveEntityDeltaPacket.setHeadYaw(headYaw);
+            this.headYaw = headYaw;
         }
-        setRotation(rotation);
 
         if (!moveEntityDeltaPacket.getFlags().isEmpty()) {
             session.sendUpstreamPacket(moveEntityDeltaPacket);
@@ -108,14 +112,12 @@ public class ThrowableEntity extends Entity implements Tickable {
     /**
      * Get the gravity of this entity type. Used for applying gravity while the entity is in motion.
      *
-     * @param session the session of the Bedrock client.
      * @return the amount of gravity to apply to this entity while in motion.
      */
-    protected float getGravity(GeyserSession session) {
-        if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY)) {
-            switch (entityType) {
+    protected float getGravity() {
+        if (getFlag(EntityFlag.HAS_GRAVITY)) {
+            switch (definition.entityType()) {
                 case THROWN_POTION:
-                case LINGERING_POTION:
                     return 0.05f;
                 case THROWN_EXP_BOTTLE:
                     return 0.07f;
@@ -134,16 +136,14 @@ public class ThrowableEntity extends Entity implements Tickable {
     }
 
     /**
-     * @param session the session of the Bedrock client.
      * @return the drag that should be multiplied to the entity's motion
      */
-    protected float getDrag(GeyserSession session) {
-        if (isInWater(session)) {
+    protected float getDrag() {
+        if (isInWater()) {
             return 0.8f;
         } else {
-            switch (entityType) {
+            switch (definition.entityType()) {
                 case THROWN_POTION:
-                case LINGERING_POTION:
                 case THROWN_EXP_BOTTLE:
                 case SNOWBALL:
                 case THROWN_EGG:
@@ -162,34 +162,33 @@ public class ThrowableEntity extends Entity implements Tickable {
     }
 
     /**
-     * @param session the session of the Bedrock client.
      * @return true if this entity is currently in water.
      */
-    protected boolean isInWater(GeyserSession session) {
+    protected boolean isInWater() {
         int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
         return BlockStateValues.getWaterLevel(block) != -1;
     }
 
     @Override
-    public boolean despawnEntity(GeyserSession session) {
-        if (entityType == EntityType.THROWN_ENDERPEARL) {
+    public boolean despawnEntity() {
+        if (definition.entityType() == EntityType.THROWN_ENDERPEARL) {
             LevelEventPacket particlePacket = new LevelEventPacket();
             particlePacket.setType(LevelEventType.PARTICLE_TELEPORT);
             particlePacket.setPosition(position);
             session.sendUpstreamPacket(particlePacket);
         }
-        return super.despawnEntity(session);
+        return super.despawnEntity();
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
-        moveAbsoluteImmediate(session, lastJavaPosition.add(relX, relY, relZ), rotation, isOnGround, false);
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+        moveAbsoluteImmediate(lastJavaPosition.add(relX, relY, relZ), yaw, pitch, headYaw, isOnGround, false);
         lastJavaPosition = position;
     }
 
     @Override
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
-        moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported);
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
+        moveAbsoluteImmediate(position, yaw, pitch, headYaw, isOnGround, teleported);
         lastJavaPosition = position;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/ThrowableItemEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ThrowableItemEntity.java
index 38a6204d4..d1800f716 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableItemEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableItemEntity.java
@@ -27,9 +27,10 @@ package org.geysermc.connector.entity;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 /**
  * Used as a class for any projectile entity that looks like an item
  */
@@ -40,37 +41,37 @@ public class ThrowableItemEntity extends ThrowableEntity {
     private int age;
     private boolean invisible;
 
-    public ThrowableItemEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-        metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
+    public ThrowableItemEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+        setFlag(EntityFlag.INVISIBLE, true);
         invisible = false;
     }
 
-    private void checkVisibility(GeyserSession session) {
-        if (invisible != metadata.getFlags().getFlag(EntityFlag.INVISIBLE)) {
+    private void checkVisibility() {
+        if (invisible != getFlag(EntityFlag.INVISIBLE)) {
             if (!invisible) {
                 Vector3f playerPos = session.getPlayerEntity().getPosition();
                 // Prevent projectiles from blocking the player's screen
                 if (age >= 4 || position.distanceSquared(playerPos) > 16) {
-                    metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false);
-                    updateBedrockMetadata(session);
+                    setFlag(EntityFlag.INVISIBLE, false);
+                    updateBedrockMetadata();
                 }
             } else {
-                metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
-                updateBedrockMetadata(session);
+                setFlag(EntityFlag.INVISIBLE, true);
+                updateBedrockMetadata();
             }
         }
         age++;
     }
 
     @Override
-    public void tick(GeyserSession session) {
-        checkVisibility(session);
-        super.tick(session);
+    public void tick() {
+        checkVisibility();
+        super.tick();
     }
 
     @Override
-    protected void setInvisible(GeyserSession session, boolean value) {
+    protected void setInvisible(boolean value) {
         invisible = value;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/ThrownPotionEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ThrownPotionEntity.java
index 578a460b6..6f5b6a88e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/ThrownPotionEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/ThrownPotionEntity.java
@@ -27,50 +27,44 @@ package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
 import com.github.steveice10.opennbt.tag.builtin.StringTag;
 import com.github.steveice10.opennbt.tag.builtin.Tag;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import org.geysermc.connector.GeyserConnector;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.item.Potion;
 import org.geysermc.connector.registry.type.ItemMapping;
 
 import java.util.EnumSet;
+import java.util.UUID;
 
 public class ThrownPotionEntity extends ThrowableItemEntity {
     private static final EnumSet<Potion> NON_ENCHANTED_POTIONS = EnumSet.of(Potion.WATER, Potion.MUNDANE, Potion.THICK, Potion.AWKWARD);
 
-    public ThrownPotionEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ThrownPotionEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8 && entityMetadata.getType() == MetadataType.ITEM) {
-            ItemStack itemStack = (ItemStack) entityMetadata.getValue();
-            ItemMapping mapping = session.getItemMappings().getMapping(itemStack);
-            if (mapping.getJavaIdentifier().endsWith("potion") && itemStack.getNbt() != null) {
-                Tag potionTag = itemStack.getNbt().get("Potion");
-                if (potionTag instanceof StringTag) {
-                    Potion potion = Potion.getByJavaIdentifier(((StringTag) potionTag).getValue());
-                    if (potion != null) {
-                        metadata.put(EntityData.POTION_AUX_VALUE, potion.getBedrockId());
-                        metadata.getFlags().setFlag(EntityFlag.ENCHANTED, !NON_ENCHANTED_POTIONS.contains(potion));
-                    } else {
-                        metadata.put(EntityData.POTION_AUX_VALUE, 0);
-                        GeyserConnector.getInstance().getLogger().debug("Unknown java potion: " + potionTag.getValue());
-                    }
+    public void setPotion(EntityMetadata<ItemStack> entityMetadata) {
+        ItemStack itemStack = entityMetadata.getValue();
+        ItemMapping mapping = session.getItemMappings().getMapping(itemStack);
+        if (mapping.getJavaIdentifier().endsWith("potion") && itemStack.getNbt() != null) {
+            Tag potionTag = itemStack.getNbt().get("Potion");
+            if (potionTag instanceof StringTag) {
+                Potion potion = Potion.getByJavaIdentifier(((StringTag) potionTag).getValue());
+                if (potion != null) {
+                    dirtyMetadata.put(EntityData.POTION_AUX_VALUE, potion.getBedrockId());
+                    setFlag(EntityFlag.ENCHANTED, !NON_ENCHANTED_POTIONS.contains(potion));
+                } else {
+                    dirtyMetadata.put(EntityData.POTION_AUX_VALUE, 0);
+                    GeyserConnector.getInstance().getLogger().debug("Unknown java potion: " + potionTag.getValue());
                 }
-
-                boolean isLingering = mapping.getJavaIdentifier().equals("minecraft:lingering_potion");
-                metadata.getFlags().setFlag(EntityFlag.LINGERING, isLingering);
             }
-        }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+            boolean isLingering = mapping.getJavaIdentifier().equals("minecraft:lingering_potion");
+            setFlag(EntityFlag.LINGERING, isLingering);
+        }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/Tickable.java b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java
index a7d571ccb..f5f0188fc 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/Tickable.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java
@@ -25,11 +25,9 @@
 
 package org.geysermc.connector.entity;
 
-import org.geysermc.connector.network.session.GeyserSession;
-
 /**
  * Implemented onto anything that should have code ran every Minecraft tick - 50 milliseconds.
  */
 public interface Tickable {
-    void tick(GeyserSession session);
+    void tick();
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/TippedArrowEntity.java b/connector/src/main/java/org/geysermc/connector/entity/TippedArrowEntity.java
index f7b63435f..d05cb5d2a 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/TippedArrowEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/TippedArrowEntity.java
@@ -26,38 +26,35 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.item.TippedArrowPotion;
 
+import java.util.UUID;
+
 /**
  * Internally this is known as TippedArrowEntity but is used with tipped arrows and normal arrows
  */
 public class TippedArrowEntity extends AbstractArrowEntity {
 
-    public TippedArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public TippedArrowEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        // Arrow potion effect color
-        if (entityMetadata.getId() == 10) {
-            int potionColor = (int) entityMetadata.getValue();
-            // -1 means no color
-            if (potionColor == -1) {
-                metadata.put(EntityData.CUSTOM_DISPLAY, 0);
+    public void setPotionEffectColor(EntityMetadata<Integer> entityMetadata) {
+        int potionColor = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        // -1 means no color
+        if (potionColor == -1) {
+            dirtyMetadata.put(EntityData.CUSTOM_DISPLAY, 0);
+        } else {
+            TippedArrowPotion potion = TippedArrowPotion.getByJavaColor(potionColor);
+            if (potion != null && potion.getJavaColor() != -1) {
+                dirtyMetadata.put(EntityData.CUSTOM_DISPLAY, (byte) potion.getBedrockId());
             } else {
-                TippedArrowPotion potion = TippedArrowPotion.getByJavaColor(potionColor);
-                if (potion != null && potion.getJavaColor() != -1) {
-                    metadata.put(EntityData.CUSTOM_DISPLAY, (byte) potion.getBedrockId());
-                } else {
-                    metadata.put(EntityData.CUSTOM_DISPLAY, 0);
-                }
+                dirtyMetadata.put(EntityData.CUSTOM_DISPLAY, 0);
             }
         }
-        super.updateBedrockMetadata(entityMetadata, session);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/TridentEntity.java b/connector/src/main/java/org/geysermc/connector/entity/TridentEntity.java
index 19ec27692..5c53ce357 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/TridentEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/TridentEntity.java
@@ -25,24 +25,14 @@
 
 package org.geysermc.connector.entity;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class TridentEntity extends AbstractArrowEntity {
 
-    public TridentEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
-
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 11) {
-            metadata.getFlags().setFlag(EntityFlag.ENCHANTED, (boolean) entityMetadata.getValue());
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
+    public TridentEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java
index 6209538dc..936a6c091 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java
@@ -26,35 +26,34 @@
 package org.geysermc.connector.entity;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class WitherSkullEntity extends ItemedFireballEntity {
     private boolean isCharged;
 
-    public WitherSkullEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public WitherSkullEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
         this.futureTicks = 1;
     }
 
-    @Override
-    protected float getDrag(GeyserSession session) {
-        return isCharged ? 0.73f : super.getDrag(session);
+    public void setDangerous(EntityMetadata<Boolean> entityMetadata) {
+        boolean newDangerous = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        if (newDangerous != isCharged) {
+            isCharged = newDangerous;
+            // Is an entirely new entity in Bedrock but just a metadata type in Java
+            definition = isCharged ? EntityDefinitions.WITHER_SKULL_DANGEROUS : EntityDefinitions.WITHER_SKULL;
+            despawnEntity();
+            spawnEntity();
+        }
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 8) {
-            boolean newIsCharged = (boolean) entityMetadata.getValue();
-            if (newIsCharged != isCharged) {
-                isCharged = newIsCharged;
-                entityType = isCharged ? EntityType.WITHER_SKULL_DANGEROUS : EntityType.WITHER_SKULL;
-                despawnEntity(session);
-                spawnEntity(session);
-            }
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    protected float getDrag() {
+        return isCharged ? 0.73f : super.getDrag();
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/factory/BaseEntityFactory.java b/connector/src/main/java/org/geysermc/connector/entity/factory/BaseEntityFactory.java
new file mode 100644
index 000000000..67b0e5609
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/entity/factory/BaseEntityFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.entity.factory;
+
+import com.nukkitx.math.vector.Vector3f;
+import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
+
+@FunctionalInterface
+public interface BaseEntityFactory<T extends Entity> extends EntityFactory<T> {
+
+    T create(GeyserSession session, long javaId, long bedrockId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw);
+}
diff --git a/connector/src/main/java/org/geysermc/connector/entity/factory/EntityFactory.java b/connector/src/main/java/org/geysermc/connector/entity/factory/EntityFactory.java
new file mode 100644
index 000000000..542b9a018
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/entity/factory/EntityFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.entity.factory;
+
+import org.geysermc.connector.entity.Entity;
+
+public interface EntityFactory<T extends Entity> {
+}
diff --git a/connector/src/main/java/org/geysermc/connector/entity/factory/ExperienceOrbEntityFactory.java b/connector/src/main/java/org/geysermc/connector/entity/factory/ExperienceOrbEntityFactory.java
new file mode 100644
index 000000000..41d8db1c8
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/entity/factory/ExperienceOrbEntityFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.entity.factory;
+
+import com.nukkitx.math.vector.Vector3f;
+import org.geysermc.connector.entity.ExpOrbEntity;
+import org.geysermc.connector.network.session.GeyserSession;
+
+@FunctionalInterface
+public interface ExperienceOrbEntityFactory extends EntityFactory<ExpOrbEntity> {
+
+    ExpOrbEntity create(GeyserSession session, int amount, long entityId, long geyserId, Vector3f position);
+}
diff --git a/connector/src/main/java/org/geysermc/connector/entity/factory/PaintingEntityFactory.java b/connector/src/main/java/org/geysermc/connector/entity/factory/PaintingEntityFactory.java
new file mode 100644
index 000000000..95bac4c44
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/entity/factory/PaintingEntityFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.entity.factory;
+
+import com.nukkitx.math.vector.Vector3f;
+import org.geysermc.connector.entity.PaintingEntity;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.utils.PaintingType;
+
+import java.util.UUID;
+
+public interface PaintingEntityFactory extends EntityFactory<PaintingEntity> {
+
+    PaintingEntity create(GeyserSession session, long entityId, long geyserId, UUID uuid, Vector3f position, PaintingType paintingName, int direction);
+}
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/AbstractFishEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/AbstractFishEntity.java
index 3b80c05fc..43bbf4544 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/AbstractFishEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/AbstractFishEntity.java
@@ -27,16 +27,19 @@ package org.geysermc.connector.entity.living;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class AbstractFishEntity extends WaterEntity {
 
-    public AbstractFishEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AbstractFishEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
-        metadata.getFlags().setFlag(EntityFlag.CAN_SWIM, true);
-        metadata.getFlags().setFlag(EntityFlag.BREATHING, true);
-        metadata.getFlags().setFlag(EntityFlag.CAN_CLIMB, false);
-        metadata.getFlags().setFlag(EntityFlag.HAS_GRAVITY, false);
+        setFlag(EntityFlag.CAN_SWIM, true);
+        setFlag(EntityFlag.BREATHING, true);
+        setFlag(EntityFlag.CAN_CLIMB, false);
+        setFlag(EntityFlag.HAS_GRAVITY, false);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/AgeableEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/AgeableEntity.java
index 8f8166d07..b864b8e7b 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/AgeableEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/AgeableEntity.java
@@ -26,29 +26,46 @@
 package org.geysermc.connector.entity.living;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class AgeableEntity extends CreatureEntity {
 
-    public AgeableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AgeableEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            boolean isBaby = (boolean) entityMetadata.getValue();
-            metadata.put(EntityData.SCALE, isBaby ? .55f : 1f);
-            metadata.getFlags().setFlag(EntityFlag.BABY, isBaby);
+    public void setBaby(EntityMetadata<Boolean> entityMetadata) {
+        boolean isBaby = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.SCALE, isBaby ? getBabySize() : getAdultSize());
+        setFlag(EntityFlag.BABY, isBaby);
 
-            metadata.put(EntityData.BOUNDING_BOX_HEIGHT, entityType.getHeight() * (isBaby ? 0.55f : 1f));
-            metadata.put(EntityData.BOUNDING_BOX_WIDTH, entityType.getWidth() * (isBaby ? 0.55f : 1f));
-        }
+        // TODO save this?
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, definition.height() * (isBaby ? getBabySize() : getAdultSize()));
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, definition.width() * (isBaby ? getBabySize() : getAdultSize()));
+    }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+    /**
+     * The scale that should be used when this entity is not a baby.
+     */
+    protected float getAdultSize() {
+        return 1f;
+    }
+
+    /**
+     * The scale that should be used when this entity is a baby.
+     */
+    protected float getBabySize() {
+        return 0.55f;
+    }
+
+    public boolean isBaby() {
+        return getFlag(EntityFlag.BABY);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/AmbientEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/AmbientEntity.java
index cc5fd2111..38e3483a6 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/AmbientEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/AmbientEntity.java
@@ -26,11 +26,14 @@
 package org.geysermc.connector.entity.living;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
 
-public class AmbientEntity extends InsentientEntity {
+import java.util.UUID;
 
-    public AmbientEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+public class AmbientEntity extends MobEntity {
+
+    public AmbientEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java
index 6a87097a1..904de233f 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java
@@ -26,18 +26,22 @@
 package org.geysermc.connector.entity.living;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Rotation;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
 import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
 import lombok.Getter;
+import net.kyori.adventure.text.Component;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.LivingEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class ArmorStandEntity extends LivingEntity {
 
     // These are used to store the state of the armour stand for use when handling invisibility
@@ -72,162 +76,150 @@ public class ArmorStandEntity extends LivingEntity {
      * Whether we should update the position of this armor stand after metadata updates.
      */
     private boolean positionUpdateRequired = false;
-    private GeyserSession session;
 
-    public ArmorStandEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ArmorStandEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        this.session = session;
-        this.rotation = Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX());
-        super.spawnEntity(session);
+    public void spawnEntity() {
+        this.pitch = yaw;
+        this.headYaw = yaw;
+        super.spawnEntity();
     }
 
     @Override
-    public boolean despawnEntity(GeyserSession session) {
+    public boolean despawnEntity() {
         if (secondEntity != null) {
-            secondEntity.despawnEntity(session);
+            secondEntity.despawnEntity();
         }
-        return super.despawnEntity(session);
+        return super.despawnEntity();
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
         if (secondEntity != null) {
-            secondEntity.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
+            secondEntity.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
         }
-        super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
+        super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
     }
 
     @Override
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
         if (secondEntity != null) {
-            secondEntity.moveAbsolute(session, applyOffsetToPosition(position), rotation, isOnGround, teleported);
+            secondEntity.moveAbsolute(applyOffsetToPosition(position), yaw, pitch, headYaw, isOnGround, teleported);
         } else if (positionRequiresOffset) {
             // Fake the height to be above where it is so the nametag appears in the right location for invisible non-marker armour stands
             position = applyOffsetToPosition(position);
         }
 
-        super.moveAbsolute(session, position, Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX()), isOnGround, teleported);
+        super.moveAbsolute(position, yaw, yaw, yaw, isOnGround, teleported);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        if (entityMetadata.getId() == 2) {
+    public void setDisplayName(EntityMetadata<Component> entityMetadata) {
+        super.setDisplayName(entityMetadata);
+        updateSecondEntityStatus(false);
+    }
+
+    public void setArmorStandFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+
+        // isSmall
+        boolean newIsSmall = (xd & 0x01) == 0x01;
+        if (newIsSmall != isSmall) {
+            if (positionRequiresOffset) {
+                // Fix new inconsistency with offset
+                this.position = fixOffsetForSize(position, newIsSmall);
+                positionUpdateRequired = true;
+            }
+
+            isSmall = newIsSmall;
+            if (!isMarker) {
+                toggleSmallStatus();
+            }
+        }
+
+        // setMarker
+        boolean oldIsMarker = isMarker;
+        isMarker = (xd & 0x10) == 0x10;
+        if (oldIsMarker != isMarker) {
+            if (isMarker) {
+                dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.0f);
+                dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f);
+                dirtyMetadata.put(EntityData.SCALE, 0f);
+            } else {
+                toggleSmallStatus();
+            }
+
             updateSecondEntityStatus(false);
-        } else if (entityMetadata.getId() == 15 && entityMetadata.getType() == MetadataType.BYTE) {
-            byte xd = (byte) entityMetadata.getValue();
-
-            // isSmall
-            boolean newIsSmall = (xd & 0x01) == 0x01;
-            if (newIsSmall != isSmall) {
-                if (positionRequiresOffset) {
-                    // Fix new inconsistency with offset
-                    this.position = fixOffsetForSize(position, newIsSmall);
-                    positionUpdateRequired = true;
-                }
-
-                isSmall = newIsSmall;
-                if (!isMarker) {
-                    toggleSmallStatus();
-                }
-            }
-
-            // setMarker
-            boolean oldIsMarker = isMarker;
-            isMarker = (xd & 0x10) == 0x10;
-            if (oldIsMarker != isMarker) {
-                if (isMarker) {
-                    metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.0f);
-                    metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f);
-                    metadata.put(EntityData.SCALE, 0f);
-                } else {
-                    toggleSmallStatus();
-                }
-
-                updateSecondEntityStatus(false);
-            }
-
-            // The following values don't do anything on normal Bedrock.
-            // But if given a resource pack, then we can use these values to control armor stand visual properties
-            metadata.getFlags().setFlag(EntityFlag.ANGRY, (xd & 0x04) != 0x04); // Has arms
-            metadata.getFlags().setFlag(EntityFlag.ADMIRING, (xd & 0x08) == 0x08); // Has no baseplate
-        } else {
-            EntityData dataLeech = null;
-            EntityFlag negativeXToggle = null;
-            EntityFlag negativeYToggle = null;
-            EntityFlag negativeZToggle = null;
-            switch (entityMetadata.getId()) {
-                case 16 -> { // Head
-                    dataLeech = EntityData.MARK_VARIANT;
-                    negativeXToggle = EntityFlag.INTERESTED;
-                    negativeYToggle = EntityFlag.CHARGED;
-                    negativeZToggle = EntityFlag.POWERED;
-                }
-                case 17 -> { // Body
-                    dataLeech = EntityData.VARIANT;
-                    negativeXToggle = EntityFlag.IN_LOVE;
-                    negativeYToggle = EntityFlag.CELEBRATING;
-                    negativeZToggle = EntityFlag.CELEBRATING_SPECIAL;
-                }
-                case 18 -> { // Left arm
-                    dataLeech = EntityData.TRADE_TIER;
-                    negativeXToggle = EntityFlag.CHARGING;
-                    negativeYToggle = EntityFlag.CRITICAL;
-                    negativeZToggle = EntityFlag.DANCING;
-                }
-                case 19 -> { // Right arm
-                    dataLeech = EntityData.MAX_TRADE_TIER;
-                    negativeXToggle = EntityFlag.ELDER;
-                    negativeYToggle = EntityFlag.EMOTING;
-                    negativeZToggle = EntityFlag.IDLING;
-                }
-                case 20 -> { // Left leg
-                    dataLeech = EntityData.SKIN_ID;
-                    negativeXToggle = EntityFlag.IS_ILLAGER_CAPTAIN;
-                    negativeYToggle = EntityFlag.IS_IN_UI;
-                    negativeZToggle = EntityFlag.LINGERING;
-                }
-                case 21 -> { // Right leg
-                    dataLeech = EntityData.HURT_DIRECTION;
-                    negativeXToggle = EntityFlag.IS_PREGNANT;
-                    negativeYToggle = EntityFlag.SHEARED;
-                    negativeZToggle = EntityFlag.STALKING;
-                }
-            }
-            if (dataLeech != null) {
-                // Indicate that rotation should be checked
-                metadata.getFlags().setFlag(EntityFlag.BRIBED, true);
-
-                Rotation rotation = (Rotation) entityMetadata.getValue();
-                int rotationX = getRotation(rotation.getPitch());
-                int rotationY = getRotation(rotation.getYaw());
-                int rotationZ = getRotation(rotation.getRoll());
-                // The top bit acts like binary and determines if each rotation goes above 100
-                // We don't do this for the negative values out of concerns of the number being too big
-                int topBit = (Math.abs(rotationX) >= 100 ? 4 : 0) + (Math.abs(rotationY) >= 100 ? 2 : 0) + (Math.abs(rotationZ) >= 100 ? 1 : 0);
-                int value = (topBit * 1000000) + ((Math.abs(rotationX) % 100) * 10000) + ((Math.abs(rotationY) % 100) * 100) + (Math.abs(rotationZ) % 100);
-                metadata.put(dataLeech, value);
-                // Set the entity flags if a value is negative
-                metadata.getFlags().setFlag(negativeXToggle, rotationX < 0);
-                metadata.getFlags().setFlag(negativeYToggle, rotationY < 0);
-                metadata.getFlags().setFlag(negativeZToggle, rotationZ < 0);
-            }
-        }
-        if (secondEntity != null) {
-            secondEntity.updateBedrockMetadata(entityMetadata, session);
         }
+
+        // The following values don't do anything on normal Bedrock.
+        // But if given a resource pack, then we can use these values to control armor stand visual properties
+        setFlag(EntityFlag.ANGRY, (xd & 0x04) != 0x04); // Has arms
+        setFlag(EntityFlag.ADMIRING, (xd & 0x08) == 0x08); // Has no baseplate
+    }
+
+    public void setHeadRotation(EntityMetadata<Rotation> entityMetadata) {
+        onRotationUpdate(EntityData.MARK_VARIANT, EntityFlag.INTERESTED, EntityFlag.CHARGED, EntityFlag.POWERED, entityMetadata.getValue());
+    }
+
+    public void setBodyRotation(EntityMetadata<Rotation> entityMetadata) {
+        onRotationUpdate(EntityData.VARIANT, EntityFlag.IN_LOVE, EntityFlag.CELEBRATING, EntityFlag.CELEBRATING_SPECIAL, entityMetadata.getValue());
+    }
+
+    public void setLeftArmRotation(EntityMetadata<Rotation> entityMetadata) {
+        onRotationUpdate(EntityData.TRADE_TIER, EntityFlag.CHARGING, EntityFlag.CRITICAL, EntityFlag.DANCING, entityMetadata.getValue());
+    }
+
+    public void setRightArmRotation(EntityMetadata<Rotation> entityMetadata) {
+        onRotationUpdate(EntityData.MAX_TRADE_TIER, EntityFlag.ELDER, EntityFlag.EMOTING, EntityFlag.IDLING, entityMetadata.getValue());
+    }
+
+    public void setLeftLegRotation(EntityMetadata<Rotation> entityMetadata) {
+        onRotationUpdate(EntityData.SKIN_ID, EntityFlag.IS_ILLAGER_CAPTAIN, EntityFlag.IS_IN_UI, EntityFlag.LINGERING, entityMetadata.getValue());
+    }
+
+    public void setRightLegRotation(EntityMetadata<Rotation> entityMetadata) {
+        onRotationUpdate(EntityData.HURT_DIRECTION, EntityFlag.IS_PREGNANT, EntityFlag.SHEARED, EntityFlag.STALKING, entityMetadata.getValue());
+    }
+
+    /**
+     * Updates rotation on the armor stand by hijacking other unused Bedrock entity data/flags.
+     * Do note: as of recent Bedrock versions there is a custom entity data system that can be replaced with this,
+     * but at this time there is no need to implement this.
+     *
+     * @param dataLeech the entity data to "leech" off of that stores a compressed version of the rotation
+     * @param negativeXToggle the flag to set true if the X value of rotation is negative
+     * @param negativeYToggle the flag to set true if the Y value of rotation is negative
+     * @param negativeZToggle the flag to set true if the Z value of rotation is negative
+     * @param rotation the Java rotation value
+     */
+    private void onRotationUpdate(EntityData dataLeech, EntityFlag negativeXToggle, EntityFlag negativeYToggle, EntityFlag negativeZToggle, Rotation rotation) {
+        // Indicate that rotation should be checked
+        setFlag(EntityFlag.BRIBED, true);
+
+        int rotationX = getRotation(rotation.getPitch());
+        int rotationY = getRotation(rotation.getYaw());
+        int rotationZ = getRotation(rotation.getRoll());
+        // The top bit acts like binary and determines if each rotation goes above 100
+        // We don't do this for the negative values out of concerns of the number being too big
+        int topBit = (Math.abs(rotationX) >= 100 ? 4 : 0) + (Math.abs(rotationY) >= 100 ? 2 : 0) + (Math.abs(rotationZ) >= 100 ? 1 : 0);
+        int value = (topBit * 1000000) + ((Math.abs(rotationX) % 100) * 10000) + ((Math.abs(rotationY) % 100) * 100) + (Math.abs(rotationZ) % 100);
+        dirtyMetadata.put(dataLeech, value);
+        // Set the entity flags if a value is negative
+        setFlag(negativeXToggle, rotationX < 0);
+        setFlag(negativeYToggle, rotationY < 0);
+        setFlag(negativeZToggle, rotationZ < 0);
     }
 
     @Override
-    public void updateBedrockMetadata(GeyserSession session) {
+    public void updateBedrockMetadata() {
         if (secondEntity != null) {
-            secondEntity.updateBedrockMetadata(session);
+            secondEntity.updateBedrockMetadata();
         }
-        super.updateBedrockMetadata(session);
+        super.updateBedrockMetadata();
         if (positionUpdateRequired) {
             positionUpdateRequired = false;
             updatePosition();
@@ -235,7 +227,7 @@ public class ArmorStandEntity extends LivingEntity {
     }
 
     @Override
-    protected void setInvisible(GeyserSession session, boolean value) {
+    protected void setInvisible(boolean value) {
         // Check if the armour stand is invisible and store accordingly
         if (primaryEntity) {
             isInvisible = value;
@@ -289,7 +281,7 @@ public class ArmorStandEntity extends LivingEntity {
         if (!primaryEntity) return;
         if (!isInvisible || isMarker) {
             // It is either impossible to show armor, or the armor stand isn't invisible. We good.
-            metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false);
+            setFlag(EntityFlag.INVISIBLE, false);
             updateOffsetRequirement(false);
             if (positionUpdateRequired) {
                 positionUpdateRequired = false;
@@ -297,13 +289,13 @@ public class ArmorStandEntity extends LivingEntity {
             }
 
             if (secondEntity != null) {
-                secondEntity.despawnEntity(session);
+                secondEntity.despawnEntity();
                 secondEntity = null;
             }
             return;
         }
         //boolean isNametagEmpty = metadata.getString(EntityData.NAMETAG).isEmpty() || metadata.getByte(EntityData.NAMETAG_ALWAYS_SHOW, (byte) -1) == (byte) 0; - may not be necessary?
-        boolean isNametagEmpty = metadata.getString(EntityData.NAMETAG).isEmpty();
+        boolean isNametagEmpty = dirtyMetadata.getString(EntityData.NAMETAG).isEmpty(); // TODO
         if (!isNametagEmpty && (!helmet.equals(ItemData.AIR) || !chestplate.equals(ItemData.AIR) || !leggings.equals(ItemData.AIR)
                 || !boots.equals(ItemData.AIR) || !hand.equals(ItemData.AIR) || !offHand.equals(ItemData.AIR))) {
             // If the second entity exists, no need to recreate it.
@@ -312,8 +304,8 @@ public class ArmorStandEntity extends LivingEntity {
 
             // Create the second entity. It doesn't need to worry about the items, but it does need to worry about
             // the metadata as it will hold the name tag.
-            secondEntity = new ArmorStandEntity(0, session.getEntityCache().getNextEntityId().incrementAndGet(),
-                    EntityType.ARMOR_STAND, position, motion, rotation);
+            secondEntity = new ArmorStandEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(), null,
+                    EntityDefinitions.ARMOR_STAND, position, motion, yaw, pitch, headYaw);
             secondEntity.primaryEntity = false;
             if (!this.positionRequiresOffset) {
                 // Ensure the offset is applied for the 0 scale
@@ -321,52 +313,51 @@ public class ArmorStandEntity extends LivingEntity {
             }
             // Copy metadata
             secondEntity.isSmall = isSmall;
-            secondEntity.getMetadata().putAll(metadata);
-            // Copy the flags so they aren't the same object in memory
-            secondEntity.getMetadata().putFlags(metadata.getFlags().copy());
+            secondEntity.getDirtyMetadata().putAll(dirtyMetadata); //TODO check
+            secondEntity.flags.merge(this.flags);
             // Guarantee this copy is NOT invisible
-            secondEntity.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false);
+            secondEntity.setFlag(EntityFlag.INVISIBLE, false);
             // Scale to 0 to show nametag
-            secondEntity.getMetadata().put(EntityData.SCALE, 0.0f);
+            secondEntity.getDirtyMetadata().put(EntityData.SCALE, 0.0f);
             // No bounding box as we don't want to interact with this entity
-            secondEntity.getMetadata().put(EntityData.BOUNDING_BOX_WIDTH, 0.0f);
-            secondEntity.getMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f);
-            secondEntity.spawnEntity(session);
+            secondEntity.getDirtyMetadata().put(EntityData.BOUNDING_BOX_WIDTH, 0.0f);
+            secondEntity.getDirtyMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f);
+            secondEntity.spawnEntity();
 
             // Reset scale of the proper armor stand
-            this.metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
+            this.dirtyMetadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
             // Set the proper armor stand to invisible to show armor
-            this.metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
+            setFlag(EntityFlag.INVISIBLE, true);
             // Update the position of the armor stand
             updateOffsetRequirement(false);
         } else if (isNametagEmpty) {
             // We can just make an invisible entity
             // Reset scale of the proper armor stand
-            metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
+            dirtyMetadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
             // Set the proper armor stand to invisible to show armor
-            metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
+            setFlag(EntityFlag.INVISIBLE, true);
             // Update offset
             updateOffsetRequirement(false);
 
             if (secondEntity != null) {
-                secondEntity.despawnEntity(session);
+                secondEntity.despawnEntity();
                 secondEntity = null;
             }
         } else {
             // Nametag is not empty and there is no armor
             // We don't need to make a new entity
-            metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false);
-            metadata.put(EntityData.SCALE, 0.0f);
+            setFlag(EntityFlag.INVISIBLE, false);
+            dirtyMetadata.put(EntityData.SCALE, 0.0f);
             // As the above is applied, we need an offset
             updateOffsetRequirement(true);
 
             if (secondEntity != null) {
-                secondEntity.despawnEntity(session);
+                secondEntity.despawnEntity();
                 secondEntity = null;
             }
         }
         if (sendMetadata) {
-            this.updateBedrockMetadata(session);
+            this.updateBedrockMetadata();
         }
     }
 
@@ -385,16 +376,16 @@ public class ArmorStandEntity extends LivingEntity {
      * If this armor stand is not a marker, set its bounding box size and scale.
      */
     private void toggleSmallStatus() {
-        metadata.put(EntityData.BOUNDING_BOX_WIDTH, isSmall ? 0.25f : entityType.getWidth());
-        metadata.put(EntityData.BOUNDING_BOX_HEIGHT, isSmall ? 0.9875f : entityType.getHeight());
-        metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, isSmall ? 0.25f : definition.width());
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, isSmall ? 0.9875f : definition.height());
+        dirtyMetadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
     }
 
     /**
      * @return the selected position with the position offset applied.
      */
     private Vector3f applyOffsetToPosition(Vector3f position) {
-        return position.add(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d);
+        return position.add(0d, definition.height() * (isSmall ? 0.55d : 1d), 0d);
     }
 
     /**
@@ -402,14 +393,14 @@ public class ArmorStandEntity extends LivingEntity {
      */
     private Vector3f fixOffsetForSize(Vector3f position, boolean isNowSmall) {
         position = removeOffsetFromPosition(position);
-        return position.add(0d, entityType.getHeight() * (isNowSmall ? 0.55d : 1d), 0d);
+        return position.add(0d, definition.height() * (isNowSmall ? 0.55d : 1d), 0d);
     }
 
     /**
      * @return the selected position with the position offset removed.
      */
     private Vector3f removeOffsetFromPosition(Vector3f position) {
-        return position.sub(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d);
+        return position.sub(0d, definition.height() * (isSmall ? 0.55d : 1d), 0d);
     }
 
     /**
@@ -434,7 +425,7 @@ public class ArmorStandEntity extends LivingEntity {
         MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
         moveEntityPacket.setRuntimeEntityId(geyserId);
         moveEntityPacket.setPosition(position);
-        moveEntityPacket.setRotation(Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX()));
+        moveEntityPacket.setRotation(Vector3f.from(yaw, yaw, yaw));
         moveEntityPacket.setOnGround(onGround);
         moveEntityPacket.setTeleported(false);
         session.sendUpstreamPacket(moveEntityPacket);
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/BatEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/BatEntity.java
index 110c02e06..4f5bd1ead 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/BatEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/BatEntity.java
@@ -26,23 +26,22 @@
 package org.geysermc.connector.entity.living;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class BatEntity extends AmbientEntity {
 
-    public BatEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public BatEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.RESTING, (xd & 0x01) == 0x01);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setBatFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.RESTING, (xd & 0x01) == 0x01);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/CreatureEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/CreatureEntity.java
index e2cc5a6f4..e0407e0fc 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/CreatureEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/CreatureEntity.java
@@ -26,11 +26,14 @@
 package org.geysermc.connector.entity.living;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
 
-public class CreatureEntity extends InsentientEntity {
+import java.util.UUID;
 
-    public CreatureEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+public class CreatureEntity extends MobEntity {
+
+    public CreatureEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/FlyingEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/FlyingEntity.java
index 6bcfe79f1..1cf362985 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/FlyingEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/FlyingEntity.java
@@ -26,11 +26,14 @@
 package org.geysermc.connector.entity.living;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
 
-public class FlyingEntity extends InsentientEntity {
+import java.util.UUID;
 
-    public FlyingEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+public class FlyingEntity extends MobEntity {
+
+    public FlyingEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/GlowSquidEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/GlowSquidEntity.java
index b68c5d224..f99c9ee4d 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/GlowSquidEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/GlowSquidEntity.java
@@ -25,19 +25,14 @@
 
 package org.geysermc.connector.entity.living;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
-public class GlowSquidEntity extends SquidEntity {
-    public GlowSquidEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
+import java.util.UUID;
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        // TODO "dark ticks remaining" ??? does this have a Bedrock equivalent?
+public class GlowSquidEntity extends SquidEntity {
+    public GlowSquidEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/GolemEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/GolemEntity.java
index 2f202ee9f..da88f5acc 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/GolemEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/GolemEntity.java
@@ -26,11 +26,14 @@
 package org.geysermc.connector.entity.living;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class GolemEntity extends CreatureEntity {
 
-    public GolemEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public GolemEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/IronGolemEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/IronGolemEntity.java
index a7b7a55c6..21e495239 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/IronGolemEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/IronGolemEntity.java
@@ -28,15 +28,18 @@ package org.geysermc.connector.entity.living;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class IronGolemEntity extends GolemEntity {
 
-    public IronGolemEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public IronGolemEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
         // Indicate that we should show cracks through a resource pack
-        metadata.getFlags().setFlag(EntityFlag.BRIBED, true);
+        setFlag(EntityFlag.BRIBED, true);
         // Required, or else the overlay is black
-        metadata.put(EntityData.COLOR_2, (byte) 0);
+        dirtyMetadata.put(EntityData.COLOR_2, (byte) 0);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/MagmaCubeEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/MagmaCubeEntity.java
index fb2726d1d..bdbac85a7 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/MagmaCubeEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/MagmaCubeEntity.java
@@ -27,32 +27,34 @@ package org.geysermc.connector.entity.living;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class MagmaCubeEntity extends SlimeEntity {
 
-    public MagmaCubeEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public MagmaCubeEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
-        updateJump(session, isOnGround);
-        super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+        updateJump(isOnGround);
+        super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
     }
 
     @Override
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
-        updateJump(session, isOnGround);
-        super.moveAbsolute(session, position, rotation, isOnGround, teleported);
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
+        updateJump(isOnGround);
+        super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported);
     }
 
-    public void updateJump(GeyserSession session, boolean newOnGround) {
+    public void updateJump(boolean newOnGround) {
         if (newOnGround != onGround) {
             // Add the jumping effect to the magma cube
-            metadata.put(EntityData.CLIENT_EVENT, (byte) (newOnGround ? 1 : 2));
-            updateBedrockMetadata(session);
+            dirtyMetadata.put(EntityData.CLIENT_EVENT, (byte) (newOnGround ? 1 : 2));
+            updateBedrockMetadata();
         }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/MobEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/MobEntity.java
new file mode 100644
index 000000000..3c603a61e
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/MobEntity.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.entity.living;
+
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.nukkitx.math.vector.Vector3f;
+import com.nukkitx.protocol.bedrock.data.entity.EntityData;
+import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import lombok.Getter;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.entity.LivingEntity;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
+
+public class MobEntity extends LivingEntity {
+    /**
+     * 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, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    @Override
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        setLeashHolderBedrockId(-1);
+    }
+
+    public void setMobFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.NO_AI, (xd & 0x01) == 0x01);
+    }
+
+    public void setLeashHolderBedrockId(long bedrockId) {
+        this.leashHolderBedrockId = bedrockId;
+        dirtyMetadata.put(EntityData.LEASH_HOLDER_EID, bedrockId);
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/SlimeEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/SlimeEntity.java
index f08fcc796..b533a6613 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/SlimeEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/SlimeEntity.java
@@ -26,22 +26,21 @@
 package org.geysermc.connector.entity.living;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
-public class SlimeEntity extends InsentientEntity {
+import java.util.UUID;
 
-    public SlimeEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+public class SlimeEntity extends MobEntity {
+
+    public SlimeEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            this.metadata.put(EntityData.SCALE, 0.10f + (int) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setScale(EntityMetadata<Integer> entityMetadata) {
+        dirtyMetadata.put(EntityData.SCALE, 0.10f + ((IntEntityMetadata) entityMetadata).getPrimitiveValue());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/SnowGolemEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/SnowGolemEntity.java
index 6bfb23564..7dafd4853 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/SnowGolemEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/SnowGolemEntity.java
@@ -26,24 +26,23 @@
 package org.geysermc.connector.entity.living;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class SnowGolemEntity extends GolemEntity {
 
-    public SnowGolemEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public SnowGolemEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            byte xd = (byte) entityMetadata.getValue();
-            // Handle the visibility of the pumpkin
-            metadata.getFlags().setFlag(EntityFlag.SHEARED, (xd & 0x10) != 0x10);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setSnowGolemFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        // Handle the visibility of the pumpkin
+        setFlag(EntityFlag.SHEARED, (xd & 0x10) != 0x10);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/SquidEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/SquidEntity.java
index bf18b97a0..18c1ec9a8 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/SquidEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/SquidEntity.java
@@ -28,28 +28,25 @@ package org.geysermc.connector.entity.living;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.MoveEntityDeltaPacket;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.Tickable;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.world.block.BlockStateValues;
 
+import java.util.UUID;
+
 public class SquidEntity extends WaterEntity implements Tickable {
-
-    private float pitch;
-    private float yaw;
-
     private float targetPitch;
     private float targetYaw;
 
     private boolean inWater;
 
-    public SquidEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-        this.yaw = rotation.getX();
+    public SquidEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void tick(GeyserSession session) {
+    public void tick() {
         boolean pitchChanged;
         boolean yawChanged;
         float oldPitch = pitch;
@@ -82,25 +79,33 @@ public class SquidEntity extends WaterEntity implements Tickable {
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
-        super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
-        checkInWater(session);
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+        super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
+        checkInWater();
     }
 
     @Override
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
-        super.moveAbsolute(session, position, rotation, isOnGround, teleported);
-        checkInWater(session);
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
+        super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported);
+        checkInWater();
     }
 
     @Override
-    public void setRotation(Vector3f rotation) {
+    public void setYaw(float yaw) {
         // Let the Java server control yaw when the squid is out of water
         if (!inWater) {
-            yaw = rotation.getX();
+            this.yaw = yaw;
         }
     }
 
+    @Override
+    public void setPitch(float pitch) {
+    }
+
+    @Override
+    public void setHeadYaw(float headYaw) {
+    }
+
     @Override
     public void setMotion(Vector3f motion) {
         super.setMotion(motion);
@@ -115,8 +120,8 @@ public class SquidEntity extends WaterEntity implements Tickable {
         return Vector3f.from(pitch, yaw, yaw);
     }
 
-    private void checkInWater(GeyserSession session) {
-        if (getMetadata().getFlags().getFlag(EntityFlag.RIDING)) {
+    private void checkInWater() {
+        if (getFlag(EntityFlag.RIDING)) {
             inWater = false;
         } else {
             int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/WaterEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/WaterEntity.java
index 9b90ba72e..4766841ac 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/WaterEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/WaterEntity.java
@@ -26,11 +26,14 @@
 package org.geysermc.connector.entity.living;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class WaterEntity extends CreatureEntity {
 
-    public WaterEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public WaterEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/AnimalEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/AnimalEntity.java
index 2495eab0a..7ef8f107e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/AnimalEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/AnimalEntity.java
@@ -26,15 +26,17 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.nukkitx.math.vector.Vector3f;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.AgeableEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class AnimalEntity extends AgeableEntity {
 
-    public AnimalEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AnimalEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     /**
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/AxolotlEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/AxolotlEntity.java
index 0968cecb0..53c70d13e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/AxolotlEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/AxolotlEntity.java
@@ -26,32 +26,33 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class AxolotlEntity extends AnimalEntity {
-    public AxolotlEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AxolotlEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        if (entityMetadata.getId() == 17) {
-            int variant = (int) entityMetadata.getValue();
-            switch (variant) {
-                case 1 -> variant = 3; // Java - "Wild" (brown)
-                case 3 -> variant = 1; // Java - cyan
-            }
-            metadata.put(EntityData.VARIANT, variant);
-        }
-        else if (entityMetadata.getId() == 18) {
-            metadata.getFlags().setFlag(EntityFlag.PLAYING_DEAD, (boolean) entityMetadata.getValue());
+    public void setVariant(EntityMetadata<Integer> entityMetadata) {
+        int variant = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        switch (variant) {
+            case 1 -> variant = 3; // Java - "Wild" (brown)
+            case 3 -> variant = 1; // Java - cyan
         }
+        dirtyMetadata.put(EntityData.VARIANT, variant);
+    }
+
+    public void setPlayingDead(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.PLAYING_DEAD, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/BeeEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/BeeEntity.java
index 07136252a..12366c4cd 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/BeeEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/BeeEntity.java
@@ -26,43 +26,44 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class BeeEntity extends AnimalEntity {
 
-    public BeeEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public BeeEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            byte xd = (byte) entityMetadata.getValue();
-            // Bee is performing sting attack; trigger animation
-            if ((xd & 0x02) == 0x02) {
-                EntityEventPacket packet = new EntityEventPacket();
-                packet.setRuntimeEntityId(geyserId);
-                packet.setType(EntityEventType.ATTACK_START);
-                packet.setData(0);
-                session.sendUpstreamPacket(packet);
-            }
-            // If the bee has stung
-            metadata.put(EntityData.MARK_VARIANT, (xd & 0x04) == 0x04 ? 1 : 0);
-            // If the bee has nectar or not
-            metadata.getFlags().setFlag(EntityFlag.POWERED, (xd & 0x08) == 0x08);
+    public void setBeeFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        // Bee is performing sting attack; trigger animation
+        if ((xd & 0x02) == 0x02) {
+            EntityEventPacket packet = new EntityEventPacket();
+            packet.setRuntimeEntityId(geyserId);
+            packet.setType(EntityEventType.ATTACK_START);
+            packet.setData(0);
+            session.sendUpstreamPacket(packet);
         }
-        if (entityMetadata.getId() == 18) {
-            // Converting "anger time" to a boolean
-            metadata.getFlags().setFlag(EntityFlag.ANGRY, (int) entityMetadata.getValue() > 0);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+        // If the bee has stung
+        dirtyMetadata.put(EntityData.MARK_VARIANT, (xd & 0x04) == 0x04 ? 1 : 0);
+        // If the bee has nectar or not
+        setFlag(EntityFlag.POWERED, (xd & 0x08) == 0x08);
+    }
+
+    public void setAngerTime(EntityMetadata<Integer> entityMetadata) {
+        // Converting "anger time" to a boolean
+        setFlag(EntityFlag.ANGRY, ((IntEntityMetadata) entityMetadata).getPrimitiveValue() > 0);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/ChickenEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/ChickenEntity.java
index c7eb62c6e..b1031209f 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/ChickenEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/ChickenEntity.java
@@ -26,14 +26,16 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class ChickenEntity extends AnimalEntity {
 
-    public ChickenEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ChickenEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/FoxEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/FoxEntity.java
index 26da89612..7016753ba 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/FoxEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/FoxEntity.java
@@ -26,32 +26,32 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class FoxEntity extends AnimalEntity {
 
-    public FoxEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public FoxEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            metadata.put(EntityData.VARIANT, entityMetadata.getValue());
-        }
-        if (entityMetadata.getId() == 18) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SITTING, (xd & 0x01) == 0x01);
-            metadata.getFlags().setFlag(EntityFlag.SNEAKING, (xd & 0x04) == 0x04);
-            metadata.getFlags().setFlag(EntityFlag.INTERESTED, (xd & 0x08) == 0x08);
-            metadata.getFlags().setFlag(EntityFlag.SLEEPING, (xd & 0x20) == 0x20);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setFoxVariant(EntityMetadata<Integer> entityMetadata) {
+        dirtyMetadata.put(EntityData.VARIANT, entityMetadata.getValue());
+    }
+
+    public void setFoxFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SITTING, (xd & 0x01) == 0x01);
+        setFlag(EntityFlag.SNEAKING, (xd & 0x04) == 0x04);
+        setFlag(EntityFlag.INTERESTED, (xd & 0x08) == 0x08);
+        setFlag(EntityFlag.SLEEPING, (xd & 0x20) == 0x20);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/GoatEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/GoatEntity.java
index a43998f27..6bf7cd336 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/GoatEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/GoatEntity.java
@@ -27,12 +27,15 @@ package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import lombok.Getter;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class GoatEntity extends AnimalEntity {
     private static final float LONG_JUMPING_HEIGHT = 1.3f * 0.7f;
     private static final float LONG_JUMPING_WIDTH = 0.9f * 0.7f;
@@ -40,24 +43,20 @@ public class GoatEntity extends AnimalEntity {
     @Getter
     private boolean isScreamer;
 
-    public GoatEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public GoatEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        if (entityMetadata.getId() == 17) {
-            // Not used in Bedrock Edition
-            isScreamer = (boolean) entityMetadata.getValue();
-        }
+    public void setScreamer(EntityMetadata<Boolean> entityMetadata) {
+        // Metadata not used in Bedrock Edition
+        isScreamer = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
     }
 
     @Override
     protected void setDimensions(Pose pose) {
         if (pose == Pose.LONG_JUMPING) {
-            metadata.put(EntityData.BOUNDING_BOX_WIDTH, LONG_JUMPING_WIDTH);
-            metadata.put(EntityData.BOUNDING_BOX_HEIGHT, LONG_JUMPING_HEIGHT);
+            dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, LONG_JUMPING_WIDTH);
+            dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, LONG_JUMPING_HEIGHT);
         } else {
             super.setDimensions(pose);
         }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/HoglinEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/HoglinEntity.java
index d782dc53e..6493810c4 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/HoglinEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/HoglinEntity.java
@@ -26,34 +26,32 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 import org.geysermc.connector.utils.DimensionUtils;
 
+import java.util.UUID;
+
 public class HoglinEntity extends AnimalEntity {
     private boolean isImmuneToZombification;
 
-    public HoglinEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public HoglinEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setImmuneToZombification(EntityMetadata<Boolean> entityMetadata) {
+        // Apply shaking effect if not in the nether and zombification is possible
+        this.isImmuneToZombification = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SHAKING, isShaking());
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            // Immune to zombification?
-            // Apply shaking effect if not in the nether and zombification is possible
-            this.isImmuneToZombification = (boolean) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SHAKING, isShaking(session));
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    protected boolean isShaking(GeyserSession session) {
-        return (!isImmuneToZombification && !session.getDimension().equals(DimensionUtils.NETHER)) || super.isShaking(session);
+    protected boolean isShaking() {
+        return (!isImmuneToZombification && !session.getDimension().equals(DimensionUtils.NETHER)) || super.isShaking();
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/MooshroomEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/MooshroomEntity.java
index e2e22c73d..2047b8afe 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/MooshroomEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/MooshroomEntity.java
@@ -25,23 +25,15 @@
 
 package org.geysermc.connector.entity.living.animal;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class MooshroomEntity extends AnimalEntity {
 
-    public MooshroomEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
-
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            metadata.put(EntityData.VARIANT, entityMetadata.getValue().equals("brown") ? 1 : 0);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public MooshroomEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/OcelotEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/OcelotEntity.java
index 98e7140cd..ed4f8133b 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/OcelotEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/OcelotEntity.java
@@ -25,25 +25,17 @@
 
 package org.geysermc.connector.entity.living.animal;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class OcelotEntity extends AnimalEntity {
 
-    public OcelotEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
-
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            metadata.getFlags().setFlag(EntityFlag.TRUSTING, (boolean) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public OcelotEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PandaEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PandaEntity.java
index 39049d91a..ff66d30a8 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PandaEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PandaEntity.java
@@ -26,57 +26,60 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
-public class PandaEntity extends AnimalEntity {
+import java.util.UUID;
 
+public class PandaEntity extends AnimalEntity {
     private int mainGene;
     private int hiddenGene;
 
-    public PandaEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public PandaEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 19) {
-            metadata.getFlags().setFlag(EntityFlag.EATING, (int) entityMetadata.getValue() > 0);
-            metadata.put(EntityData.EATING_COUNTER, entityMetadata.getValue());
-            if ((int) entityMetadata.getValue() != 0) {
-                // Particles and sound
-                EntityEventPacket packet = new EntityEventPacket();
-                packet.setRuntimeEntityId(geyserId);
-                packet.setType(EntityEventType.EATING_ITEM);
-                packet.setData(session.getItemMappings().getStoredItems().bamboo().getBedrockId() << 16);
-                session.sendUpstreamPacket(packet);
-            }
+    public void setEatingCounter(EntityMetadata<Integer> entityMetadata) {
+        int count = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.EATING, count > 0);
+        dirtyMetadata.put(EntityData.EATING_COUNTER, count);
+        if (count != 0) {
+            // Particles and sound
+            EntityEventPacket packet = new EntityEventPacket();
+            packet.setRuntimeEntityId(geyserId);
+            packet.setType(EntityEventType.EATING_ITEM);
+            packet.setData(session.getItemMappings().getStoredItems().bamboo().getBedrockId() << 16);
+            session.sendUpstreamPacket(packet);
         }
-        if (entityMetadata.getId() == 20) {
-            mainGene = (int) (byte) entityMetadata.getValue();
-            updateAppearance();
-        }
-        if (entityMetadata.getId() == 21) {
-            hiddenGene = (int) (byte) entityMetadata.getValue();
-            updateAppearance();
-        }
-        if (entityMetadata.getId() == 22) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SNEEZING, (xd & 0x02) == 0x02);
-            metadata.getFlags().setFlag(EntityFlag.ROLLING, (xd & 0x04) == 0x04);
-            metadata.getFlags().setFlag(EntityFlag.SITTING, (xd & 0x08) == 0x08);
-            // Required to put these both for sitting to actually show
-            metadata.put(EntityData.SITTING_AMOUNT, (xd & 0x08) == 0x08 ? 1f : 0f);
-            metadata.put(EntityData.SITTING_AMOUNT_PREVIOUS, (xd & 0x08) == 0x08 ? 1f : 0f);
-            metadata.getFlags().setFlag(EntityFlag.LAYING_DOWN, (xd & 0x10) == 0x10);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    }
+
+    public void setMainGene(EntityMetadata<Byte> entityMetadata) {
+        mainGene = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        updateAppearance();
+    }
+
+    public void setHiddenGene(EntityMetadata<Byte> entityMetadata) {
+        hiddenGene = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        updateAppearance();
+    }
+
+    public void setPandaFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SNEEZING, (xd & 0x02) == 0x02);
+        setFlag(EntityFlag.ROLLING, (xd & 0x04) == 0x04);
+        setFlag(EntityFlag.SITTING, (xd & 0x08) == 0x08);
+        // Required to put these both for sitting to actually show
+        dirtyMetadata.put(EntityData.SITTING_AMOUNT, (xd & 0x08) == 0x08 ? 1f : 0f);
+        dirtyMetadata.put(EntityData.SITTING_AMOUNT_PREVIOUS, (xd & 0x08) == 0x08 ? 1f : 0f);
+        setFlag(EntityFlag.LAYING_DOWN, (xd & 0x10) == 0x10);
     }
 
     @Override
@@ -93,14 +96,14 @@ public class PandaEntity extends AnimalEntity {
             // Main gene is a recessive trait
             if (mainGene == hiddenGene) {
                 // Main and hidden genes match; this is what the panda looks like.
-                metadata.put(EntityData.VARIANT, mainGene);
+                dirtyMetadata.put(EntityData.VARIANT, mainGene);
             } else {
                 // Genes have no effect on appearance
-                metadata.put(EntityData.VARIANT, 0);
+                dirtyMetadata.put(EntityData.VARIANT, 0);
             }
         } else {
             // No need to worry about hidden gene
-            metadata.put(EntityData.VARIANT, mainGene);
+            dirtyMetadata.put(EntityData.VARIANT, mainGene);
         }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PigEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PigEntity.java
index ba6bfd553..8fe4fde06 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PigEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PigEntity.java
@@ -25,25 +25,17 @@
 
 package org.geysermc.connector.entity.living.animal;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class PigEntity extends AnimalEntity {
 
-    public PigEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
-
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            metadata.getFlags().setFlag(EntityFlag.SADDLED, (boolean) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public PigEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PolarBearEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PolarBearEntity.java
index 893def315..4eb5d79fc 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PolarBearEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PolarBearEntity.java
@@ -25,25 +25,17 @@
 
 package org.geysermc.connector.entity.living.animal;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class PolarBearEntity extends AnimalEntity {
 
-    public PolarBearEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
-
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            metadata.getFlags().setFlag(EntityFlag.STANDING, (boolean) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public PolarBearEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PufferFishEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PufferFishEntity.java
index 0ddd581f8..e2c9039de 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/PufferFishEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/PufferFishEntity.java
@@ -26,25 +26,24 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.AbstractFishEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class PufferFishEntity extends AbstractFishEntity {
 
-    public PufferFishEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public PufferFishEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            int puffsize = (int) entityMetadata.getValue();
-            metadata.put(EntityData.PUFFERFISH_SIZE, (byte) puffsize);
-            metadata.put(EntityData.VARIANT, puffsize);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setPufferfishSize(EntityMetadata<Integer> entityMetadata) {
+        int puffsize = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.PUFFERFISH_SIZE, (byte) puffsize);
+        dirtyMetadata.put(EntityData.VARIANT, puffsize);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java
index 0f0e1fb3e..2d6a0d96a 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java
@@ -26,42 +26,49 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class RabbitEntity extends AnimalEntity {
 
-    public RabbitEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public RabbitEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        if (entityMetadata.getId() == 16) {
-            metadata.put(EntityData.SCALE, .55f);
-            boolean isBaby = (boolean) entityMetadata.getValue();
-            if (isBaby) {
-                metadata.put(EntityData.SCALE, .35f);
-                metadata.getFlags().setFlag(EntityFlag.BABY, true);
-            }
-        } else if (entityMetadata.getId() == 17) {
-            int variant = (int) entityMetadata.getValue();
+    public void setBaby(EntityMetadata<Boolean> entityMetadata) {
+        super.setBaby(entityMetadata);
+    }
 
-            // Change the killer bunny to display as white since it only exists on Java Edition
-            boolean isKillerBunny = variant == 99;
-            if (isKillerBunny) {
-                variant = 1;
-            }
-            // Allow the resource pack to adjust to the killer bunny
-            metadata.getFlags().setFlag(EntityFlag.BRIBED, isKillerBunny);
+    public void setRabbitVariant(EntityMetadata<Integer> entityMetadata) {
+        int variant = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
 
-            metadata.put(EntityData.VARIANT, variant);
+        // Change the killer bunny to display as white since it only exists on Java Edition
+        boolean isKillerBunny = variant == 99;
+        if (isKillerBunny) {
+            variant = 1;
         }
+        // Allow the resource pack to adjust to the killer bunny
+        setFlag(EntityFlag.BRIBED, isKillerBunny);
+
+        dirtyMetadata.put(EntityData.VARIANT, variant);
+    }
+
+    @Override
+    protected float getAdultSize() {
+        return 0.55f;
+    }
+
+    @Override
+    protected float getBabySize() {
+        return 0.35f;
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/SheepEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/SheepEntity.java
index f723eff2b..c063188c5 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/SheepEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/SheepEntity.java
@@ -26,26 +26,24 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class SheepEntity extends AnimalEntity {
 
-    public SheepEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public SheepEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SHEARED, (xd & 0x10) == 0x10);
-            metadata.put(EntityData.COLOR, xd);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setSheepFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SHEARED, (xd & 0x10) == 0x10);
+        dirtyMetadata.put(EntityData.COLOR, xd);
     }
 }
\ No newline at end of file
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/StriderEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/StriderEntity.java
index 83c1e3674..d31bf4a4e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/StriderEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/StriderEntity.java
@@ -26,70 +26,70 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import org.geysermc.connector.entity.Entity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class StriderEntity extends AnimalEntity {
 
     private boolean isCold = false;
 
-    public StriderEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public StriderEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
-        metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
-        metadata.getFlags().setFlag(EntityFlag.BREATHING, true);
+        setFlag(EntityFlag.FIRE_IMMUNE, true);
+        setFlag(EntityFlag.BREATHING, true);
+    }
+
+    public void setCold(EntityMetadata<Boolean> entityMetadata) {
+        isCold = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+    }
+
+    public void setSaddled(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 18) {
-            isCold = (boolean) entityMetadata.getValue();
-        }
-        if (entityMetadata.getId() == 19) {
-            metadata.getFlags().setFlag(EntityFlag.SADDLED, (boolean) entityMetadata.getValue());
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    public void updateBedrockMetadata(GeyserSession session) {
+    public void updateBedrockMetadata() {
         // Make sure they are not shaking when riding another entity
         // Needs to copy the parent state
-        if (metadata.getFlags().getFlag(EntityFlag.RIDING)) {
+        if (getFlag(EntityFlag.RIDING)) {
             boolean parentShaking = false;
+            //TODO optimize
             for (Entity ent : session.getEntityCache().getEntities().values()) {
                 if (ent.getPassengers().contains(entityId) && ent instanceof StriderEntity) {
-                    parentShaking = ent.getMetadata().getFlags().getFlag(EntityFlag.SHAKING);
+                    parentShaking = ent.getFlag(EntityFlag.SHAKING);
                     break;
                 }
             }
     
-            metadata.getFlags().setFlag(EntityFlag.BREATHING, !parentShaking);
-            metadata.getFlags().setFlag(EntityFlag.SHAKING, parentShaking);
+            setFlag(EntityFlag.BREATHING, !parentShaking);
+            setFlag(EntityFlag.SHAKING, parentShaking);
         } else {
-            metadata.getFlags().setFlag(EntityFlag.BREATHING, !isCold);
-            metadata.getFlags().setFlag(EntityFlag.SHAKING, isShaking(session));
+            setFlag(EntityFlag.BREATHING, !isCold);
+            setFlag(EntityFlag.SHAKING, isShaking());
         }
 
         // Update the passengers if we have any
         for (long passenger : passengers) {
             Entity passengerEntity = session.getEntityCache().getEntityByJavaId(passenger);
             if (passengerEntity != null) {
-                passengerEntity.updateBedrockMetadata(session);
+                passengerEntity.updateBedrockMetadata();
             }
         }
 
-        super.updateBedrockMetadata(session);
+        super.updateBedrockMetadata();
     }
 
     @Override
-    protected boolean isShaking(GeyserSession session) {
-        return isCold || super.isShaking(session);
+    protected boolean isShaking() {
+        return isCold || super.isShaking();
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/TropicalFishEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/TropicalFishEntity.java
index d16eb2ece..cd652f6a3 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/TropicalFishEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/TropicalFishEntity.java
@@ -26,15 +26,17 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.google.common.collect.ImmutableList;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import it.unimi.dsi.fastutil.ints.IntList;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.AbstractFishEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
 import java.util.List;
+import java.util.UUID;
 
 public class TropicalFishEntity extends AbstractFishEntity {
 
@@ -47,21 +49,17 @@ public class TropicalFishEntity extends AbstractFishEntity {
     private static final List<String> VARIANT_NAMES = ImmutableList.of("kob", "sunstreak", "snooper", "dasher", "brinely", "spotty", "flopper", "stripey", "glitter", "blockfish", "betty", "clayfish");
     private static final List<String> COLOR_NAMES = ImmutableList.of("white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black");
 
-    public TropicalFishEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public TropicalFishEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            int varNumber = (int) entityMetadata.getValue();
+    public void setFishVariant(EntityMetadata<Integer> entityMetadata) {
+        int varNumber = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
 
-            metadata.put(EntityData.VARIANT, getShape(varNumber)); // Shape 0-1
-            metadata.put(EntityData.MARK_VARIANT, getPattern(varNumber)); // Pattern 0-5
-            metadata.put(EntityData.COLOR, getBaseColor(varNumber)); // Base color 0-15
-            metadata.put(EntityData.COLOR_2, getPatternColor(varNumber)); // Pattern color 0-15
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+        dirtyMetadata.put(EntityData.VARIANT, getShape(varNumber)); // Shape 0-1
+        dirtyMetadata.put(EntityData.MARK_VARIANT, getPattern(varNumber)); // Pattern 0-5
+        dirtyMetadata.put(EntityData.COLOR, getBaseColor(varNumber)); // Base color 0-15
+        dirtyMetadata.put(EntityData.COLOR_2, getPatternColor(varNumber)); // Pattern color 0-15
     }
 
     public static int getShape(int variant) {
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/TurtleEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/TurtleEntity.java
index b26b21ee2..28ff2d836 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/TurtleEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/TurtleEntity.java
@@ -26,26 +26,27 @@
 package org.geysermc.connector.entity.living.animal;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class TurtleEntity extends AnimalEntity {
 
-    public TurtleEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public TurtleEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 18) {
-            metadata.getFlags().setFlag(EntityFlag.IS_PREGNANT, (boolean) entityMetadata.getValue());
-        } else if (entityMetadata.getId() == 19) {
-            metadata.getFlags().setFlag(EntityFlag.LAYING_EGG, (boolean) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setPregnant(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.IS_PREGNANT, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
+    }
+
+    public void setLayingEgg(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.LAYING_EGG, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java
index e255d2856..27a359d63 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java
@@ -26,6 +26,7 @@
 package org.geysermc.connector.entity.living.animal.horse;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.google.common.collect.ImmutableSet;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
@@ -34,13 +35,14 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
 import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
 import com.nukkitx.protocol.bedrock.packet.UpdateAttributesPacket;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.attribute.GeyserAttributeType;
 import org.geysermc.connector.entity.living.animal.AnimalEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
 import java.util.Set;
+import java.util.UUID;
 
 public class AbstractHorseEntity extends AnimalEntity {
     /**
@@ -50,18 +52,22 @@ public class AbstractHorseEntity extends AnimalEntity {
     private static final Set<String> DONKEY_AND_HORSE_FOODS = ImmutableSet.of("golden_apple", "enchanted_golden_apple",
             "golden_carrot", "sugar", "apple", "wheat", "hay_block");
 
-    public AbstractHorseEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AbstractHorseEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
         // Specifies the size of the entity's inventory. Required to place slots in the entity.
-        metadata.put(EntityData.CONTAINER_BASE_SIZE, 2);
+        dirtyMetadata.put(EntityData.CONTAINER_BASE_SIZE, getContainerBaseSize());
 
-        metadata.getFlags().setFlag(EntityFlag.WASD_CONTROLLED, true);
+        setFlag(EntityFlag.WASD_CONTROLLED, true);
+    }
+
+    protected int getContainerBaseSize() {
+        return 2;
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        super.spawnEntity(session);
+    public void spawnEntity() {
+        super.spawnEntity();
 
         // Add horse jump strength attribute to allow donkeys and mules to jump, if they don't send the attribute themselves.
         // Confirmed broken without this code by making a new donkey in vanilla 1.17.1
@@ -73,49 +79,44 @@ public class AbstractHorseEntity extends AnimalEntity {
         session.sendUpstreamPacket(attributesPacket);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            byte xd = (byte) entityMetadata.getValue();
-            boolean tamed = (xd & 0x02) == 0x02;
-            boolean saddled = (xd & 0x04) == 0x04;
-            metadata.getFlags().setFlag(EntityFlag.TAMED, tamed);
-            metadata.getFlags().setFlag(EntityFlag.SADDLED, saddled);
-            metadata.getFlags().setFlag(EntityFlag.EATING, (xd & 0x10) == 0x10);
-            metadata.getFlags().setFlag(EntityFlag.STANDING, (xd & 0x20) == 0x20);
+    public void setHorseFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        boolean tamed = (xd & 0x02) == 0x02;
+        boolean saddled = (xd & 0x04) == 0x04;
+        setFlag(EntityFlag.TAMED, tamed);
+        setFlag(EntityFlag.SADDLED, saddled);
+        setFlag(EntityFlag.EATING, (xd & 0x10) == 0x10);
+        setFlag(EntityFlag.STANDING, (xd & 0x20) == 0x20);
 
-            // HorseFlags
-            // Bred 0x10
-            // Eating 0x20
-            // Open mouth 0x80
-            int horseFlags = 0x0;
-            horseFlags = (xd & 0x40) == 0x40 ? horseFlags | 0x80 : horseFlags;
+        // HorseFlags
+        // Bred 0x10
+        // Eating 0x20
+        // Open mouth 0x80
+        int horseFlags = 0x0;
+        horseFlags = (xd & 0x40) == 0x40 ? horseFlags | 0x80 : horseFlags;
 
-            // Only set eating when we don't have mouth open so a player interaction doesn't trigger the eating animation
-            horseFlags = (xd & 0x10) == 0x10 && (xd & 0x40) != 0x40 ? horseFlags | 0x20 : horseFlags;
+        // Only set eating when we don't have mouth open so a player interaction doesn't trigger the eating animation
+        horseFlags = (xd & 0x10) == 0x10 && (xd & 0x40) != 0x40 ? horseFlags | 0x20 : horseFlags;
 
-            // Set the flags into the display item
-            metadata.put(EntityData.DISPLAY_ITEM, horseFlags);
+        // Set the flags into the display item
+        dirtyMetadata.put(EntityData.DISPLAY_ITEM, horseFlags);
 
-            // Send the eating particles
-            // We use the wheat metadata as static particles since Java
-            // doesn't send over what item was used to feed the horse
-            if ((xd & 0x40) == 0x40) {
-                EntityEventPacket entityEventPacket = new EntityEventPacket();
-                entityEventPacket.setRuntimeEntityId(geyserId);
-                entityEventPacket.setType(EntityEventType.EATING_ITEM);
-                entityEventPacket.setData(session.getItemMappings().getStoredItems().wheat().getBedrockId() << 16);
-                session.sendUpstreamPacket(entityEventPacket);
-            }
-
-            // Set container type if tamed
-            metadata.put(EntityData.CONTAINER_TYPE, tamed ? (byte) ContainerType.HORSE.getId() : (byte) 0);
-
-            // Shows the jump meter
-            metadata.getFlags().setFlag(EntityFlag.CAN_POWER_JUMP, saddled);
+        // Send the eating particles
+        // We use the wheat metadata as static particles since Java
+        // doesn't send over what item was used to feed the horse
+        if ((xd & 0x40) == 0x40) {
+            EntityEventPacket entityEventPacket = new EntityEventPacket();
+            entityEventPacket.setRuntimeEntityId(geyserId);
+            entityEventPacket.setType(EntityEventType.EATING_ITEM);
+            entityEventPacket.setData(session.getItemMappings().getStoredItems().wheat().getBedrockId() << 16);
+            session.sendUpstreamPacket(entityEventPacket);
         }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+        // Set container type if tamed
+        dirtyMetadata.put(EntityData.CONTAINER_TYPE, tamed ? (byte) ContainerType.HORSE.getId() : (byte) 0);
+
+        // Shows the jump meter
+        setFlag(EntityFlag.CAN_POWER_JUMP, saddled);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java
index e4f0cc241..5ccbb4ea7 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java
@@ -25,26 +25,20 @@
 
 package org.geysermc.connector.entity.living.animal.horse;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class ChestedHorseEntity extends AbstractHorseEntity {
 
-    public ChestedHorseEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-
-        metadata.put(EntityData.CONTAINER_BASE_SIZE, 16);
+    public ChestedHorseEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 19) {
-            metadata.getFlags().setFlag(EntityFlag.CHESTED, (boolean) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    protected int getContainerBaseSize() {
+        return 16;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/HorseEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/HorseEntity.java
index 87155005d..97354d047 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/HorseEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/HorseEntity.java
@@ -26,24 +26,23 @@
 package org.geysermc.connector.entity.living.animal.horse;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class HorseEntity extends AbstractHorseEntity {
 
-    public HorseEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public HorseEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 19) {
-            metadata.put(EntityData.VARIANT, ((int) entityMetadata.getValue()) & 255);
-            metadata.put(EntityData.MARK_VARIANT, (((int) entityMetadata.getValue()) >> 8) % 5);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setHorseVariant(EntityMetadata<Integer> entityMetadata) {
+        int value = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.VARIANT, value & 255);
+        dirtyMetadata.put(EntityData.MARK_VARIANT, (value >> 8) % 5);
     }
-
 }
\ No newline at end of file
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java
index 14e625c81..1c4e5155e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java
@@ -26,54 +26,47 @@
 package org.geysermc.connector.entity.living.animal.horse;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
 import com.nukkitx.protocol.bedrock.packet.MobArmorEquipmentPacket;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class LlamaEntity extends ChestedHorseEntity {
 
-    public LlamaEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public LlamaEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
-        metadata.put(EntityData.CONTAINER_STRENGTH_MODIFIER, 3); // Presumably 3 slots for every 1 strength
+        dirtyMetadata.put(EntityData.CONTAINER_STRENGTH_MODIFIER, 3); // Presumably 3 slots for every 1 strength
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        // Strength
-        if (entityMetadata.getId() == 20) {
-            metadata.put(EntityData.STRENGTH, entityMetadata.getValue());
+    /**
+     * Color equipped on the llama
+     */
+    public void setCarpetedColor(EntityMetadata<Integer> entityMetadata) {
+        // Bedrock treats llama decoration as armor
+        MobArmorEquipmentPacket equipmentPacket = new MobArmorEquipmentPacket();
+        equipmentPacket.setRuntimeEntityId(geyserId);
+        // -1 means no armor
+        int carpetIndex = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        if (carpetIndex > -1 && carpetIndex <= 15) {
+            // The damage value is the dye color that Java sends us, for pre-1.16.220
+            // The item is always going to be a carpet
+            equipmentPacket.setChestplate(session.getItemMappings().getCarpets().get(carpetIndex));
+        } else {
+            equipmentPacket.setChestplate(ItemData.AIR);
         }
-        // Color equipped on the llama
-        if (entityMetadata.getId() == 21) {
-            // Bedrock treats llama decoration as armor
-            MobArmorEquipmentPacket equipmentPacket = new MobArmorEquipmentPacket();
-            equipmentPacket.setRuntimeEntityId(geyserId);
-            // -1 means no armor
-            int carpetIndex = (int) entityMetadata.getValue();
-            if (carpetIndex > -1 && carpetIndex <= 15) {
-                // The damage value is the dye color that Java sends us, for pre-1.16.220
-                // The item is always going to be a carpet
-                equipmentPacket.setChestplate(session.getItemMappings().getCarpets().get(carpetIndex));
-            } else {
-                equipmentPacket.setChestplate(ItemData.AIR);
-            }
-            // Required to fill out the rest of the equipment or Bedrock ignores it, including above else statement if removing armor
-            equipmentPacket.setBoots(ItemData.AIR);
-            equipmentPacket.setHelmet(ItemData.AIR);
-            equipmentPacket.setLeggings(ItemData.AIR);
+        // Required to fill out the rest of the equipment or Bedrock ignores it, including above else statement if removing armor
+        equipmentPacket.setBoots(ItemData.AIR);
+        equipmentPacket.setHelmet(ItemData.AIR);
+        equipmentPacket.setLeggings(ItemData.AIR);
 
-            session.sendUpstreamPacket(equipmentPacket);
-        }
-        // Color of the llama
-        if (entityMetadata.getId() == 22) {
-            metadata.put(EntityData.VARIANT, entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+        session.sendUpstreamPacket(equipmentPacket);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/TraderLlamaEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/TraderLlamaEntity.java
index 77059ae35..b8bfcdca6 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/TraderLlamaEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/TraderLlamaEntity.java
@@ -27,18 +27,20 @@ package org.geysermc.connector.entity.living.animal.horse;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class TraderLlamaEntity extends LlamaEntity {
 
-    public TraderLlamaEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public TraderLlamaEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        this.metadata.put(EntityData.MARK_VARIANT, 1);
-        super.spawnEntity(session);
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        this.dirtyMetadata.put(EntityData.MARK_VARIANT, 1);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/CatEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/CatEntity.java
index f22124409..a4fe27535 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/CatEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/CatEntity.java
@@ -26,58 +26,71 @@
 package org.geysermc.connector.entity.living.animal.tameable;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class CatEntity extends TameableEntity {
 
     private byte collarColor;
 
-    public CatEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public CatEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateRotation(GeyserSession session, float yaw, float pitch, boolean isOnGround) {
-        moveRelative(session, 0, 0, 0, Vector3f.from(this.rotation.getX(), pitch, yaw), isOnGround);
+    public void updateRotation(float yaw, float pitch, boolean isOnGround) {
+        moveRelative(0, 0, 0, yaw, pitch, yaw, isOnGround);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        if (entityMetadata.getId() == 16) {
-            metadata.put(EntityData.SCALE, (boolean) entityMetadata.getValue() ? 0.4f : 0.8f);
-        } else if (entityMetadata.getId() == 17) {
-            // Update collar color if tamed
-            if (metadata.getFlags().getFlag(EntityFlag.TAMED)) {
-                metadata.put(EntityData.COLOR, collarColor);
-            }
+    protected float getAdultSize() {
+        return 0.8f;
+    }
+
+    @Override
+    protected float getBabySize() {
+        return 0.4f;
+    }
+
+    @Override
+    public void setTameableFlags(EntityMetadata<Byte> entityMetadata) {
+        super.setTameableFlags(entityMetadata);
+        // Update collar color if tamed
+        if (getFlag(EntityFlag.TAMED)) {
+            dirtyMetadata.put(EntityData.COLOR, collarColor);
         }
-        if (entityMetadata.getId() == 19) {
-            // Different colors in Java and Bedrock for some reason
-            int metadataValue = (int) entityMetadata.getValue();
-            int variantColor = switch (metadataValue) {
-                case 0 -> 8;
-                case 8 -> 0;
-                case 9 -> 10;
-                case 10 -> 9;
-                default -> metadataValue;
-            };
-            metadata.put(EntityData.VARIANT, variantColor);
-        }
-        if (entityMetadata.getId() == 20) {
-            metadata.getFlags().setFlag(EntityFlag.RESTING, (boolean) entityMetadata.getValue());
-        }
-        if (entityMetadata.getId() == 22) {
-            collarColor = (byte) (int) entityMetadata.getValue();
-            // Needed or else wild cats are a red color
-            if (metadata.getFlags().getFlag(EntityFlag.TAMED)) {
-                metadata.put(EntityData.COLOR, collarColor);
-            }
+    }
+
+    public void setCatVariant(EntityMetadata<Integer> entityMetadata) {
+        // Different colors in Java and Bedrock for some reason
+        int metadataValue = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        int variantColor = switch (metadataValue) {
+            case 0 -> 8;
+            case 8 -> 0;
+            case 9 -> 10;
+            case 10 -> 9;
+            default -> metadataValue;
+        };
+        dirtyMetadata.put(EntityData.VARIANT, variantColor);
+    }
+
+    public void setResting(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.RESTING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
+    }
+
+    public void setCollarColor(EntityMetadata<Integer> entityMetadata) {
+        collarColor = (byte) ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        // Needed or else wild cats are a red color
+        if (getFlag(EntityFlag.TAMED)) {
+            dirtyMetadata.put(EntityData.COLOR, collarColor);
         }
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/ParrotEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/ParrotEntity.java
index 1bb75931e..763d489e4 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/ParrotEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/ParrotEntity.java
@@ -25,26 +25,17 @@
 
 package org.geysermc.connector.entity.living.animal.tameable;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
-import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class ParrotEntity extends TameableEntity {
 
-    public ParrotEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
-
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        // Parrot color
-        if (entityMetadata.getId() == 19) {
-            metadata.put(EntityData.VARIANT, entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public ParrotEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/TameableEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/TameableEntity.java
index 923e13712..a07ce3b9b 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/TameableEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/TameableEntity.java
@@ -26,46 +26,51 @@
 package org.geysermc.connector.entity.living.animal.tameable;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import lombok.Getter;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.animal.AnimalEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
 import java.util.UUID;
 
 public class TameableEntity extends AnimalEntity {
+    /**
+     * Used in the interactive tag manager to track if the session player owns this entity
+     */
+    @Getter
+    protected long ownerBedrockId;
 
-    public TameableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public TameableEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SITTING, (xd & 0x01) == 0x01);
-            metadata.getFlags().setFlag(EntityFlag.ANGRY, (xd & 0x02) == 0x02);
-            metadata.getFlags().setFlag(EntityFlag.TAMED, (xd & 0x04) == 0x04);
-        }
+    public void setTameableFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SITTING, (xd & 0x01) == 0x01);
+        setFlag(EntityFlag.ANGRY, (xd & 0x02) == 0x02);
+        setFlag(EntityFlag.TAMED, (xd & 0x04) == 0x04);
+    }
 
+    public void setOwner(EntityMetadata<UUID> entityMetadata) {
         // Note: Must be set for wolf collar color to work
-        if (entityMetadata.getId() == 18) {
-            if (entityMetadata.getValue() != null) {
-                // Owner UUID of entity
-                Entity entity = session.getEntityCache().getPlayerEntity((UUID) entityMetadata.getValue());
-                // Used as both a check since the player isn't in the entity cache and a normal fallback
-                if (entity == null) {
-                    entity = session.getPlayerEntity();
-                }
-                // Translate to entity ID
-                metadata.put(EntityData.OWNER_EID, entity.getGeyserId());
-            } else {
-                metadata.put(EntityData.OWNER_EID, 0L); // Reset
+        if (entityMetadata.getValue() != null) {
+            // Owner UUID of entity
+            Entity entity = session.getEntityCache().getPlayerEntity(entityMetadata.getValue());
+            // Used as both a check since the player isn't in the entity cache and a normal fallback
+            if (entity == null) {
+                entity = session.getPlayerEntity();
             }
+            // Translate to entity ID
+            ownerBedrockId = entity.getGeyserId();
+        } else {
+            // Reset
+            ownerBedrockId = 0L;
         }
-        super.updateBedrockMetadata(entityMetadata, session);
+        dirtyMetadata.put(EntityData.OWNER_EID, ownerBedrockId);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/WolfEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/WolfEntity.java
index 183719dbb..5d6bcf1e3 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/WolfEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/tameable/WolfEntity.java
@@ -26,15 +26,18 @@
 package org.geysermc.connector.entity.living.animal.tameable;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.google.common.collect.ImmutableSet;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
 import java.util.Set;
+import java.util.UUID;
 
 public class WolfEntity extends TameableEntity {
     /**
@@ -47,49 +50,45 @@ public class WolfEntity extends TameableEntity {
 
     private byte collarColor;
 
-    public WolfEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public WolfEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        //Reset wolf color
-        if (entityMetadata.getId() == 17) {
-            byte xd = (byte) entityMetadata.getValue();
-            boolean angry = (xd & 0x02) == 0x02;
-            if (angry) {
-                metadata.put(EntityData.COLOR, (byte) 0);
-            }
+    public void setTameableFlags(EntityMetadata<Byte> entityMetadata) {
+        super.setFlags(entityMetadata);
+        // Reset wolf color
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        boolean angry = (xd & 0x02) == 0x02;
+        if (angry) {
+            dirtyMetadata.put(EntityData.COLOR, (byte) 0);
+        }
+    }
+
+    public void setCollarColor(EntityMetadata<Integer> entityMetadata) {
+        collarColor = (byte) ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        if (getFlag(EntityFlag.ANGRY)) {
+            return;
         }
 
-        // "Begging" on wiki.vg, "Interested" in Nukkit - the tilt of the head
-        if (entityMetadata.getId() == 19) {
-            metadata.getFlags().setFlag(EntityFlag.INTERESTED, (boolean) entityMetadata.getValue());
+        dirtyMetadata.put(EntityData.COLOR, collarColor);
+        if (ownerBedrockId == 0) {
+            // If a color is set and there is no owner entity ID, set one.
+            // Otherwise, the entire wolf is set to that color: https://user-images.githubusercontent.com/9083212/99209989-92691200-2792-11eb-911d-9a315c955be9.png
+            dirtyMetadata.put(EntityData.OWNER_EID, session.getPlayerEntity().getGeyserId());
         }
+    }
 
-        // Wolf collar color
-        // Relies on EntityData.OWNER_EID being set in TameableEntity.java
-        if (entityMetadata.getId() == 20 && !metadata.getFlags().getFlag(EntityFlag.ANGRY)) {
-            metadata.put(EntityData.COLOR, collarColor = (byte) (int) entityMetadata.getValue());
-            if (!metadata.containsKey(EntityData.OWNER_EID)) {
-                // If a color is set and there is no owner entity ID, set one.
-                // Otherwise, the entire wolf is set to that color: https://user-images.githubusercontent.com/9083212/99209989-92691200-2792-11eb-911d-9a315c955be9.png
-                metadata.put(EntityData.OWNER_EID, session.getPlayerEntity().getGeyserId());
-            }
-        }
-
-        // Wolf anger (1.16+)
-        if (entityMetadata.getId() == 21) {
-            metadata.getFlags().setFlag(EntityFlag.ANGRY, (int) entityMetadata.getValue() != 0);
-            metadata.put(EntityData.COLOR, (int) entityMetadata.getValue() != 0 ? (byte) 0 : collarColor);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
+    // 1.16+
+    public void setWolfAngerTime(EntityMetadata<Integer> entityMetadata) {
+        int time = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.ANGRY, time != 0);
+        dirtyMetadata.put(EntityData.COLOR, time != 0 ? (byte) 0 : collarColor);
     }
 
     @Override
     public boolean canEat(GeyserSession session, String javaIdentifierStripped, ItemMapping mapping) {
         // Cannot be a baby to eat these foods
-        return WOLF_FOODS.contains(javaIdentifierStripped) && !metadata.getFlags().getFlag(EntityFlag.BABY);
+        return WOLF_FOODS.contains(javaIdentifierStripped) && !getFlag(EntityFlag.BABY);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/AbstractMerchantEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/AbstractMerchantEntity.java
index 84b6772b0..da7e0b357 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/AbstractMerchantEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/AbstractMerchantEntity.java
@@ -26,18 +26,15 @@
 package org.geysermc.connector.entity.living.merchant;
 
 import com.nukkitx.math.vector.Vector3f;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.AgeableEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class AbstractMerchantEntity extends AgeableEntity {
 
-    public AbstractMerchantEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-    }
-
-    @Override
-    public void teleport(GeyserSession session, Vector3f position, float yaw, float pitch, boolean isOnGround) {
-        super.teleport(session, position, yaw - 180, pitch, isOnGround);
+    public AbstractMerchantEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java
index f64d9f0cd..234b065d4 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java
@@ -26,6 +26,7 @@
 package org.geysermc.connector.entity.living.merchant;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
@@ -34,12 +35,12 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
-import org.geysermc.connector.entity.type.EntityType;
+import lombok.Getter;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.BlockRegistries;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.UUID;
 
 public class VillagerEntity extends AbstractMerchantEntity {
 
@@ -79,32 +80,41 @@ public class VillagerEntity extends AbstractMerchantEntity {
         VILLAGER_REGIONS.put(6, 6);
     }
 
-    public VillagerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    private Vector3i bedPosition;
+    /**
+     * Used in the interactive tag manager
+     */
+    @Getter
+    private boolean canTradeWith;
+
+    public VillagerEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setVillagerData(EntityMetadata<VillagerData> entityMetadata) {
+        VillagerData villagerData = entityMetadata.getValue();
+        // Profession
+        int profession = VILLAGER_PROFESSIONS.get(villagerData.getProfession());
+        canTradeWith = profession != 14 && profession != 0; // Not a notwit and not professionless
+        dirtyMetadata.put(EntityData.VARIANT, profession);
+        //metadata.put(EntityData.SKIN_ID, villagerData.getType()); Looks like this is modified but for any reason?
+        // Region
+        dirtyMetadata.put(EntityData.MARK_VARIANT, VILLAGER_REGIONS.get(villagerData.getType()));
+        // Trade tier - different indexing in Bedrock
+        dirtyMetadata.put(EntityData.TRADE_TIER, villagerData.getLevel() - 1);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 18) {
-            VillagerData villagerData = (VillagerData) entityMetadata.getValue();
-            // Profession
-            metadata.put(EntityData.VARIANT, VILLAGER_PROFESSIONS.get(villagerData.getProfession()));
-            //metadata.put(EntityData.SKIN_ID, villagerData.getType()); Looks like this is modified but for any reason?
-            // Region
-            metadata.put(EntityData.MARK_VARIANT, VILLAGER_REGIONS.get(villagerData.getType()));
-            // Trade tier - different indexing in Bedrock
-            metadata.put(EntityData.TRADE_TIER, villagerData.getLevel() - 1);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public Vector3i setBedPosition(EntityMetadata<Position> entityMetadata) {
+        return bedPosition = super.setBedPosition(entityMetadata);
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
         // The bed block position, if it exists
-        Vector3i bedPosition;
-        if (!metadata.getFlags().getFlag(EntityFlag.SLEEPING) || (bedPosition = metadata.getPos(EntityData.BED_POSITION, null)) == null) {
+        if (!getFlag(EntityFlag.SLEEPING) || bedPosition == null) {
             // No need to worry about extra processing to compensate for sleeping
-            super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
+            super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
             return;
         }
         
@@ -133,7 +143,9 @@ public class VillagerEntity extends AbstractMerchantEntity {
             zOffset = .5f;
         }
 
-        setRotation(rotation);
+        setYaw(yaw);
+        setPitch(pitch);
+        setHeadYaw(headYaw);
         setOnGround(isOnGround);
         this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
 
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/AbstractSkeletonEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/AbstractSkeletonEntity.java
index bc17e27ca..9997dd968 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/AbstractSkeletonEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/AbstractSkeletonEntity.java
@@ -26,24 +26,25 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class AbstractSkeletonEntity extends MonsterEntity {
 
-    public AbstractSkeletonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AbstractSkeletonEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 15) {
-            byte xd = (byte) entityMetadata.getValue();
-            // A bit of a loophole so the hands get raised - set the target ID to its own ID
-            metadata.put(EntityData.TARGET_EID, ((xd & 4) == 4) ? geyserId : 0);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setMobFlags(EntityMetadata<Byte> entityMetadata) {
+        super.setMobFlags(entityMetadata);
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        // A bit of a loophole so the hands get raised - set the target ID to its own ID
+        dirtyMetadata.put(EntityData.TARGET_EID, ((xd & 4) == 4) ? geyserId : 0);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/BasePiglinEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/BasePiglinEntity.java
index cccdaf7d2..e092d3099 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/BasePiglinEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/BasePiglinEntity.java
@@ -26,32 +26,30 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.utils.DimensionUtils;
 
+import java.util.UUID;
+
 public class BasePiglinEntity extends MonsterEntity {
     private boolean isImmuneToZombification;
 
-    public BasePiglinEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public BasePiglinEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setImmuneToZombification(EntityMetadata<Boolean> entityMetadata) {
+        // Apply shaking effect if not in the nether and zombification is possible
+        this.isImmuneToZombification = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SHAKING, isShaking());
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            // Immune to zombification?
-            // Apply shaking effect if not in the nether and zombification is possible
-            this.isImmuneToZombification = (boolean) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SHAKING, isShaking(session));
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    protected boolean isShaking(GeyserSession session) {
-        return (!isImmuneToZombification && !session.getDimension().equals(DimensionUtils.NETHER)) || super.isShaking(session);
+    protected boolean isShaking() {
+        return (!isImmuneToZombification && !session.getDimension().equals(DimensionUtils.NETHER)) || super.isShaking();
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/BlazeEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/BlazeEntity.java
index 6e1bdce53..33afdf81d 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/BlazeEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/BlazeEntity.java
@@ -26,24 +26,22 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class BlazeEntity extends MonsterEntity {
 
-    public BlazeEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public BlazeEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.ON_FIRE, (xd & 0x01) == 0x01);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setBlazeFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.ON_FIRE, (xd & 0x01) == 0x01);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/CreeperEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/CreeperEntity.java
index a1dc02821..0f561f8d2 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/CreeperEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/CreeperEntity.java
@@ -26,38 +26,34 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
-public class CreeperEntity extends MonsterEntity {
+import java.util.UUID;
 
+public class CreeperEntity extends MonsterEntity {
     /**
-     * Whether the creeper has been ignited and is using ID 17.
-     * In this instance we ignore ID 15 since it's sending us -1 which confuses poor Bedrock.
+     * Whether the creeper has been ignited and is using {@link #setIgnited(EntityMetadata)}.
+     * In this instance we ignore {@link #setSwelling(EntityMetadata)} since it's sending us -1 which confuses poor Bedrock.
      */
     private boolean ignitedByFlintAndSteel = false;
 
-    public CreeperEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public CreeperEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            if (!ignitedByFlintAndSteel) {
-                metadata.getFlags().setFlag(EntityFlag.IGNITED, (int) entityMetadata.getValue() == 1);
-            }
-        }
-        if (entityMetadata.getId() == 17) {
-            metadata.getFlags().setFlag(EntityFlag.POWERED, (boolean) entityMetadata.getValue());
-        }
-        if (entityMetadata.getId() == 18) {
-            ignitedByFlintAndSteel = (boolean) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.IGNITED, ignitedByFlintAndSteel);
+    public void setSwelling(EntityMetadata<Integer> entityMetadata) {
+        if (!ignitedByFlintAndSteel) {
+            setFlag(EntityFlag.IGNITED, ((IntEntityMetadata) entityMetadata).getPrimitiveValue() == 1);
         }
+    }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setIgnited(EntityMetadata<Boolean> entityMetadata) {
+        ignitedByFlintAndSteel = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.IGNITED, ignitedByFlintAndSteel);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ElderGuardianEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ElderGuardianEntity.java
index bfbb74af9..ce98218cd 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ElderGuardianEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ElderGuardianEntity.java
@@ -27,14 +27,21 @@ package org.geysermc.connector.entity.living.monster;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class ElderGuardianEntity extends GuardianEntity {
 
-    public ElderGuardianEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-        // Otherwise it just looks like a normal guardian but bigger
-        metadata.getFlags().setFlag(EntityFlag.ELDER, true);
+    public ElderGuardianEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
+    @Override
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        // Otherwise it just looks like a normal guardian but bigger
+        setFlag(EntityFlag.ELDER, true);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java
index d3d6e30d5..ff149c5b7 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java
@@ -26,6 +26,7 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.LevelEventType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
@@ -33,17 +34,18 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.*;
 import lombok.Data;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.Tickable;
-import org.geysermc.connector.entity.living.InsentientEntity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.living.MobEntity;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.utils.DimensionUtils;
 
 import java.util.Random;
+import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicLong;
 
-public class EnderDragonEntity extends InsentientEntity implements Tickable {
+public class EnderDragonEntity extends MobEntity implements Tickable {
     /**
      * The Ender Dragon has multiple hit boxes, which
      * are each its own invisible entity
@@ -78,46 +80,47 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
     private float wingPosition;
     private float lastWingPosition;
 
-    public EnderDragonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-
-        metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
+    public EnderDragonEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) { // Phase
-            phase = (int) entityMetadata.getValue();
-            phaseTicks = 0;
-            metadata.getFlags().setFlag(EntityFlag.SITTING, isSitting());
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
-
-        if (entityMetadata.getId() == 9) {
-            if (phase == 9 && this.health <= 0) { // Dying phase
-                EntityEventPacket entityEventPacket = new EntityEventPacket();
-                entityEventPacket.setType(EntityEventType.ENDER_DRAGON_DEATH);
-                entityEventPacket.setRuntimeEntityId(geyserId);
-                entityEventPacket.setData(0);
-                session.sendUpstreamPacket(entityEventPacket);
-            }
-        }
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        setFlag(EntityFlag.FIRE_IMMUNE, true);
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
-        super.spawnEntity(session);
+    public void setHealth(EntityMetadata<Float> entityMetadata) {
+        super.setHealth(entityMetadata);
+        if (phase == 9 && this.health <= 0) { // Dying phase
+            EntityEventPacket entityEventPacket = new EntityEventPacket();
+            entityEventPacket.setType(EntityEventType.ENDER_DRAGON_DEATH);
+            entityEventPacket.setRuntimeEntityId(geyserId);
+            entityEventPacket.setData(0);
+            session.sendUpstreamPacket(entityEventPacket);
+        }
+    }
+
+    public void setPhase(EntityMetadata<Integer> entityMetadata) {
+        phase = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        phaseTicks = 0;
+        setFlag(EntityFlag.SITTING, isSitting());
+    }
+
+    @Override
+    public void spawnEntity() {
+        super.spawnEntity();
 
         AtomicLong nextEntityId = session.getEntityCache().getNextEntityId();
-        head = new EnderDragonPartEntity(entityId + 1, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 1, 1);
-        neck = new EnderDragonPartEntity(entityId + 2, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 3, 3);
-        body = new EnderDragonPartEntity(entityId + 3, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 5, 3);
-        leftWing = new EnderDragonPartEntity(entityId + 4, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2);
-        rightWing = new EnderDragonPartEntity(entityId + 5, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2);
+        head = new EnderDragonPartEntity(session, entityId + 1, nextEntityId.incrementAndGet(), 1, 1);
+        neck = new EnderDragonPartEntity(session, entityId + 2, nextEntityId.incrementAndGet(), 3, 3);
+        body = new EnderDragonPartEntity(session, entityId + 3, nextEntityId.incrementAndGet(), 5, 3);
+        leftWing = new EnderDragonPartEntity(session, entityId + 4, nextEntityId.incrementAndGet(), 4, 2);
+        rightWing = new EnderDragonPartEntity(session, entityId + 5, nextEntityId.incrementAndGet(), 4, 2);
         tail = new EnderDragonPartEntity[3];
         for (int i = 0; i < 3; i++) {
-            tail[i] = new EnderDragonPartEntity(entityId + 6 + i, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 2, 2);
+            tail[i] = new EnderDragonPartEntity(session, entityId + 6 + i, nextEntityId.incrementAndGet(), 2, 2);
         }
 
         allParts = new EnderDragonPartEntity[]{head, neck, body, leftWing, rightWing, tail[0], tail[1], tail[2]};
@@ -128,7 +131,7 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
 
         for (int i = 0; i < segmentHistory.length; i++) {
             segmentHistory[i] = new Segment();
-            segmentHistory[i].yaw = rotation.getZ();
+            segmentHistory[i].yaw = headYaw;
             segmentHistory[i].y = position.getY();
         }
     }
@@ -141,29 +144,27 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
     }
 
     @Override
-    public boolean despawnEntity(GeyserSession session) {
+    public boolean despawnEntity() {
         for (EnderDragonPartEntity part : allParts) {
-            part.despawnEntity(session);
+            part.despawnEntity();
         }
-        return super.despawnEntity(session);
+        return super.despawnEntity();
     }
 
     @Override
-    public void tick(GeyserSession session) {
-        effectTick(session);
-        if (!metadata.getFlags().getFlag(EntityFlag.NO_AI) && isAlive()) {
+    public void tick() {
+        effectTick();
+        if (!getFlag(EntityFlag.NO_AI) && isAlive()) {
             pushSegment();
-            updateBoundingBoxes(session);
+            updateBoundingBoxes();
         }
     }
 
     /**
      * Updates the positions of the Ender Dragon's multiple bounding boxes
-     *
-     * @param session GeyserSession.
      */
-    private void updateBoundingBoxes(GeyserSession session) {
-        Vector3f facingDir = Vector3f.createDirectionDeg(0, rotation.getZ());
+    private void updateBoundingBoxes() {
+        Vector3f facingDir = Vector3f.createDirectionDeg(0, headYaw);
         Segment baseSegment = getSegment(5);
         // Used to angle the head, neck, and tail when the dragon flies up/down
         float pitch = (float) Math.toRadians(10 * (baseSegment.getY() - getSegment(10).getY()));
@@ -182,7 +183,7 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
         neck.setPosition(facingDir.up(pitchY).mul(pitchXZ, 1, -pitchXZ).mul(5.5f).up(headDuck));
         body.setPosition(facingDir.mul(0.5f, 0f, -0.5f));
 
-        Vector3f wingPos = Vector3f.createDirectionDeg(0, 90f - rotation.getZ()).mul(4.5f).up(2f);
+        Vector3f wingPos = Vector3f.createDirectionDeg(0, 90f - headYaw).mul(4.5f).up(2f);
         rightWing.setPosition(wingPos);
         leftWing.setPosition(wingPos.mul(-1, 1, -1)); // Mirror horizontally
 
@@ -191,24 +192,23 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
             float distance = (i + 1) * 2f;
             // Curls the tail when the dragon turns
             Segment targetSegment = getSegment(12 + 2 * i);
-            float angle = rotation.getZ() + targetSegment.yaw - baseSegment.yaw;
+            float angle = headYaw + targetSegment.yaw - baseSegment.yaw;
 
             float tailYOffset = targetSegment.y - baseSegment.y - (distance + 1.5f) * pitchY + 1.5f;
             tail[i].setPosition(Vector3f.createDirectionDeg(0, angle).mul(distance).add(tailBase).mul(-pitchXZ, 1, pitchXZ).up(tailYOffset));
         }
         // Send updated positions
         for (EnderDragonPartEntity part : allParts) {
-             part.moveAbsolute(session, part.getPosition().add(position), Vector3f.ZERO, false, false);
+             part.moveAbsolute(part.getPosition().add(position), 0, 0, 0, false, false);
         }
     }
 
     /**
      * Handles the particles and sounds of the Ender Dragon
-     * @param session GeyserSession.
      */
-    private void effectTick(GeyserSession session) {
+    private void effectTick() {
         Random random = ThreadLocalRandom.current();
-        if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) {
+        if (!getFlag(EntityFlag.SILENT)) {
             if (Math.cos(wingPosition * 2f * Math.PI) <= -0.3f && Math.cos(lastWingPosition * 2f * Math.PI) >= -0.3f) {
                 PlaySoundPacket playSoundPacket = new PlaySoundPacket();
                 playSoundPacket.setSound("mob.enderdragon.flap");
@@ -219,14 +219,14 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
             }
 
             if (!isSitting() && !isHovering() && ticksTillNextGrowl-- == 0) {
-                playGrowlSound(session);
+                playGrowlSound();
                 ticksTillNextGrowl = 200 + random.nextInt(200);
             }
 
             lastWingPosition = wingPosition;
         }
         if (isAlive()) {
-            if (metadata.getFlags().getFlag(EntityFlag.NO_AI)) {
+            if (getFlag(EntityFlag.NO_AI)) {
                 wingPosition = 0.5f;
             } else if (isHovering() || isSitting()) {
                 wingPosition += 0.1f;
@@ -237,7 +237,7 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
 
             phaseTicks++;
             if (phase == 3) { // Landing Phase
-                float headHeight = head.getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT);
+                float headHeight = head.getDirtyMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT); //TODO
                 Vector3f headCenter = head.getPosition().up(headHeight * 0.5f);
 
                 for (int i = 0; i < 8; i++) {
@@ -262,7 +262,7 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
                     }
                 }
             } else if (phase == 7) { // Sitting Attacking Phase
-                playGrowlSound(session);
+                playGrowlSound();
             } else if (phase == 9) { // Dying Phase
                 // Send explosion particles as the dragon move towards the end portal
                 if (phaseTicks % 10 == 0) {
@@ -279,7 +279,7 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
         }
     }
 
-    private void playGrowlSound(GeyserSession session) {
+    private void playGrowlSound() {
         Random random = ThreadLocalRandom.current();
         PlaySoundPacket playSoundPacket = new PlaySoundPacket();
         playSoundPacket.setSound("mob.enderdragon.growl");
@@ -306,7 +306,7 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable {
      */
     private void pushSegment() {
         latestSegment = (latestSegment + 1) % segmentHistory.length;
-        segmentHistory[latestSegment].yaw = rotation.getZ();
+        segmentHistory[latestSegment].yaw = headYaw;
         segmentHistory[latestSegment].y = position.getY();
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java
index 288a3e423..ed1985057 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java
@@ -29,15 +29,17 @@ import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import org.geysermc.connector.entity.Entity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinitions;
+import org.geysermc.connector.network.session.GeyserSession;
 
 public class EnderDragonPartEntity extends Entity {
-    public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, float width, float height) {
-        super(entityId, geyserId, entityType, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO);
 
-        metadata.put(EntityData.BOUNDING_BOX_WIDTH, width);
-        metadata.put(EntityData.BOUNDING_BOX_HEIGHT, height);
-        metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
-        metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
+    public EnderDragonPartEntity(GeyserSession session, long entityId, long geyserId, float width, float height) {
+        super(session, entityId, geyserId, null, EntityDefinitions.ENDER_DRAGON_PART, Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0);
+
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, width);
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, height);
+        setFlag(EntityFlag.INVISIBLE, true);
+        setFlag(EntityFlag.FIRE_IMMUNE, true);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java
index e187d7f0b..94265f7e4 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java
@@ -26,41 +26,46 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.SoundEvent;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class EndermanEntity extends MonsterEntity {
 
-    public EndermanEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public EndermanEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        // Held block
-        if (entityMetadata.getId() == 16) {
-            metadata.put(EntityData.CARRIED_BLOCK, session.getBlockMappings().getBedrockBlockId((int) entityMetadata.getValue()));
-        }
-        // "Is screaming" - controls sound
-        if (entityMetadata.getId() == 17) {
-            if ((boolean) entityMetadata.getValue()) {
-                LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet();
-                packet.setSound(SoundEvent.STARE);
-                packet.setPosition(this.position);
-                packet.setExtraData(-1);
-                packet.setIdentifier("minecraft:enderman");
-                session.sendUpstreamPacket(packet);
-            }
+    public void setCarriedBlock(EntityMetadata<Integer> entityMetadata) {
+        dirtyMetadata.put(EntityData.CARRIED_BLOCK, session.getBlockMappings().getBedrockBlockId(((IntEntityMetadata) entityMetadata).getPrimitiveValue()));
+    }
+
+    /**
+     * Controls the screaming sound
+     */
+    public void setScreaming(EntityMetadata<Boolean> entityMetadata) {
+        //TODO see if Bedrock controls this differently
+        // Java Edition this controls which ambient sound is used
+        if (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()) {
+            LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet();
+            packet.setSound(SoundEvent.STARE);
+            packet.setPosition(this.position);
+            packet.setExtraData(-1);
+            packet.setIdentifier("minecraft:enderman");
+            session.sendUpstreamPacket(packet);
         }
+    }
+
+    public void setAngry(EntityMetadata<Boolean> entityMetadata) {
         // "Is staring/provoked" - controls visuals
-        if (entityMetadata.getId() == 18) {
-            metadata.getFlags().setFlag(EntityFlag.ANGRY, (boolean) entityMetadata.getValue());
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+        setFlag(EntityFlag.ANGRY, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/GhastEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/GhastEntity.java
index 22d91ec36..e281db30b 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/GhastEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/GhastEntity.java
@@ -26,24 +26,23 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.FlyingEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class GhastEntity extends FlyingEntity {
 
-    public GhastEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public GhastEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            // If the ghast is attacking
-            metadata.put(EntityData.CHARGE_AMOUNT, (byte) ((boolean) entityMetadata.getValue() ? 1 : 0));
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setGhastAttacking(EntityMetadata<Boolean> entityMetadata) {
+        // If the ghast is attacking
+        dirtyMetadata.put(EntityData.CHARGE_AMOUNT, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0));
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/GiantEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/GiantEntity.java
index 65b406d54..0b1f894f1 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/GiantEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/GiantEntity.java
@@ -27,13 +27,16 @@ package org.geysermc.connector.entity.living.monster;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class GiantEntity extends MonsterEntity {
 
-    public GiantEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public GiantEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
-        metadata.put(EntityData.SCALE, 6f);
+        dirtyMetadata.put(EntityData.SCALE, 6f);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/GuardianEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/GuardianEntity.java
index 0b28cd53e..caffb5d9e 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/GuardianEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/GuardianEntity.java
@@ -26,33 +26,34 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import org.geysermc.connector.entity.Entity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class GuardianEntity extends MonsterEntity {
 
-    public GuardianEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public GuardianEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            Entity entity = session.getEntityCache().getEntityByJavaId((int) entityMetadata.getValue());
-            if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue()) {
-                entity = session.getPlayerEntity();
-            }
-
-            if (entity != null) {
-                metadata.put(EntityData.TARGET_EID, entity.getGeyserId());
-            } else {
-                metadata.put(EntityData.TARGET_EID, (long) 0);
-            }
+    public void setGuardianTarget(EntityMetadata<Integer> entityMetadata) {
+        int entityId = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        Entity entity;
+        if (session.getPlayerEntity().getEntityId() == entityId) {
+            entity = session.getPlayerEntity();
+        } else {
+            entity = session.getEntityCache().getEntityByJavaId(entityId);
         }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+        if (entity != null) {
+            dirtyMetadata.put(EntityData.TARGET_EID, entity.getGeyserId());
+        } else {
+            dirtyMetadata.put(EntityData.TARGET_EID, (long) 0);
+        }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/MonsterEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/MonsterEntity.java
index 06a04d4c5..d83b72cd0 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/MonsterEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/MonsterEntity.java
@@ -26,12 +26,15 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.nukkitx.math.vector.Vector3f;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.CreatureEntity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class MonsterEntity extends CreatureEntity {
 
-    public MonsterEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public MonsterEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/PhantomEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/PhantomEntity.java
index bfa1ea724..651d1aa66 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/PhantomEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/PhantomEntity.java
@@ -26,28 +26,27 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.FlyingEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class PhantomEntity extends FlyingEntity {
-    public PhantomEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public PhantomEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) { // Size
-            int size = (int) entityMetadata.getValue();
-            float modelScale = 1f + 0.15f * size;
-            float boundsScale = (1f + (0.2f * size) / EntityType.PHANTOM.getWidth()) / modelScale;
+    public void setPhantomScale(EntityMetadata<Integer> entityMetadata) {
+        int size = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        float modelScale = 1f + 0.15f * size;
+        float boundsScale = (1f + (0.2f * size) / definition.width()) / modelScale;
 
-            metadata.put(EntityData.BOUNDING_BOX_WIDTH, boundsScale * EntityType.PHANTOM.getWidth());
-            metadata.put(EntityData.BOUNDING_BOX_HEIGHT, boundsScale * EntityType.PHANTOM.getHeight());
-            metadata.put(EntityData.SCALE, modelScale);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, boundsScale * definition.width());
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, boundsScale * definition.height());
+        dirtyMetadata.put(EntityData.SCALE, modelScale);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/PiglinEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/PiglinEntity.java
index 860f6dfd3..61fb36bf4 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/PiglinEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/PiglinEntity.java
@@ -26,44 +26,40 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class PiglinEntity extends BasePiglinEntity {
 
-    public PiglinEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public PiglinEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            boolean isBaby = (boolean) entityMetadata.getValue();
-            if (isBaby) {
-                metadata.put(EntityData.SCALE, .55f);
-                metadata.getFlags().setFlag(EntityFlag.BABY, true);
-            }
-        }
-        if (entityMetadata.getId() == 18) {
-            metadata.getFlags().setFlag(EntityFlag.CHARGING, (boolean) entityMetadata.getValue());
-        }
-        if (entityMetadata.getId() == 19) {
-            metadata.getFlags().setFlag(EntityFlag.DANCING, (boolean) entityMetadata.getValue());
-        }
+    public void setBaby(EntityMetadata<Boolean> entityMetadata) {
+        boolean isBaby = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.SCALE, isBaby? .55f : 1f);
+        setFlag(EntityFlag.BABY, isBaby);
+    }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setChargingCrossbow(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.CHARGING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
+    }
+
+    public void setDancing(EntityMetadata<Boolean> entityMetadata) {
+        setFlag(EntityFlag.DANCING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue());
     }
 
     @Override
     public void updateOffHand(GeyserSession session) {
         // Check if the Piglin is holding Gold and set the ADMIRING flag accordingly so its pose updates
-        boolean changed = metadata.getFlags().setFlag(EntityFlag.ADMIRING, session.getTagCache().shouldPiglinAdmire(session.getItemMappings().getMapping(this.offHand)));
-        if (changed) {
-            super.updateBedrockMetadata(session);
-        }
+        setFlag(EntityFlag.ADMIRING, session.getTagCache().shouldPiglinAdmire(session.getItemMappings().getMapping(this.offHand)));
+        super.updateBedrockMetadata();
 
         super.updateOffHand(session);
     }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java
index 2402e4330..e16df7211 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ShulkerEntity.java
@@ -26,44 +26,43 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockFace;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.GolemEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.utils.Direction;
+
+import java.util.UUID;
 
 public class ShulkerEntity extends GolemEntity {
 
-    public ShulkerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ShulkerEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
         // Indicate that invisibility should be fixed through the resource pack
-        metadata.getFlags().setFlag(EntityFlag.BRIBED, true);
+        setFlag(EntityFlag.BRIBED, true);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            BlockFace blockFace = (BlockFace) entityMetadata.getValue();
-            metadata.put(EntityData.SHULKER_ATTACH_FACE, (byte) blockFace.ordinal());
-        }
+    public void setAttachedFace(EntityMetadata<Direction> entityMetadata) {
+        Direction direction = entityMetadata.getValue();
+        dirtyMetadata.put(EntityData.SHULKER_ATTACH_FACE, (byte) direction.ordinal());
+    }
 
-        if (entityMetadata.getId() == 17) {
-            int height = (byte) entityMetadata.getValue();
-            metadata.put(EntityData.SHULKER_PEEK_ID, height);
-        }
+    public void setShulkerHeight(EntityMetadata<Byte> entityMetadata) {
+        int height = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.SHULKER_PEEK_ID, height);
+    }
 
-        if (entityMetadata.getId() == 18) {
-            byte color = (byte) entityMetadata.getValue();
-            if (color == 16) {
-                // 16 is default on both editions
-                metadata.put(EntityData.VARIANT, 16);
-            } else {
-                // Every other shulker color is offset 15 in bedrock edition
-                metadata.put(EntityData.VARIANT, Math.abs(color - 15));
-            }
+    public void setShulkerColor(EntityMetadata<Byte> entityMetadata) {
+        byte color = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        if (color == 16) {
+            // 16 is default on both editions
+            dirtyMetadata.put(EntityData.VARIANT, 16);
+        } else {
+            // Every other shulker color is offset 15 in bedrock edition
+            dirtyMetadata.put(EntityData.VARIANT, Math.abs(color - 15));
         }
-        super.updateBedrockMetadata(entityMetadata, session);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/SkeletonEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/SkeletonEntity.java
index 44c1d1f85..984590db1 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/SkeletonEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/SkeletonEntity.java
@@ -26,29 +26,28 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class SkeletonEntity extends AbstractSkeletonEntity {
     private boolean convertingToStray = false;
 
-    public SkeletonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public SkeletonEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setConvertingToStray(EntityMetadata<Boolean> entityMetadata) {
+        this.convertingToStray = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SHAKING, isShaking());
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        if (entityMetadata.getId() == 16) {
-            this.convertingToStray = (boolean) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SHAKING, isShaking(session));
-        }
-    }
-
-    @Override
-    protected boolean isShaking(GeyserSession session) {
+    protected boolean isShaking() {
         return convertingToStray;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/SpiderEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/SpiderEntity.java
index 65c30289a..f2f151eae 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/SpiderEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/SpiderEntity.java
@@ -26,24 +26,22 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class SpiderEntity extends MonsterEntity {
 
-    public SpiderEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public SpiderEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.WALL_CLIMBING, (xd & 0x01) == 0x01);
-        }
-
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setSpiderFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.WALL_CLIMBING, (xd & 0x01) == 0x01);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/VexEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/VexEntity.java
index 990a2f3a9..d6e05b387 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/VexEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/VexEntity.java
@@ -26,25 +26,24 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class VexEntity extends MonsterEntity {
 
-    public VexEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public VexEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            byte xd = (byte) entityMetadata.getValue();
-            // Set the target to the player to force the attack animation
-            // even if the player isn't the target as we dont get the target on Java
-            metadata.put(EntityData.TARGET_EID, (xd & 0x01) == 0x01 ? session.getPlayerEntity().getGeyserId() : 0);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setVexFlags(EntityMetadata<Byte> entityMetadata) {
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        // Set the target to the player to force the attack animation
+        // even if the player isn't the target as we dont get the target on Java
+        dirtyMetadata.put(EntityData.TARGET_EID, (xd & 0x01) == 0x01 ? session.getPlayerEntity().getGeyserId() : 0);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java
index d6d7f8074..befb464fe 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java
@@ -26,52 +26,62 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import org.geysermc.connector.entity.Entity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class WitherEntity extends MonsterEntity {
 
-    public WitherEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
-
-        metadata.put(EntityData.WITHER_AERIAL_ATTACK, (short) 1);
+    public WitherEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        long targetID = 0;
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        dirtyMetadata.put(EntityData.WITHER_AERIAL_ATTACK, (short) 1);
+    }
 
-        if (entityMetadata.getId() >= 16 && entityMetadata.getId() <= 18) {
-            Entity entity = session.getEntityCache().getEntityByJavaId((int) entityMetadata.getValue());
-            if (entity == null && session.getPlayerEntity().getEntityId() == (int) entityMetadata.getValue()) {
-                entity = session.getPlayerEntity();
-            }
+    public void setTarget1(EntityMetadata<Integer> entityMetadata) {
+        setTargetId(EntityData.WITHER_TARGET_1, entityMetadata);
+    }
 
-            if (entity != null) {
-                targetID = entity.getGeyserId();
-            }
+    public void setTarget2(EntityMetadata<Integer> entityMetadata) {
+        setTargetId(EntityData.WITHER_TARGET_2, entityMetadata);
+    }
+
+    public void setTarget3(EntityMetadata<Integer> entityMetadata) {
+        setTargetId(EntityData.WITHER_TARGET_3, entityMetadata);
+    }
+
+    private void setTargetId(EntityData entityData, EntityMetadata<Integer> entityMetadata) {
+        int entityId = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        Entity entity;
+        if (session.getPlayerEntity().getEntityId() == entityId) {
+            entity = session.getPlayerEntity();
+        } else {
+            entity = session.getEntityCache().getEntityByJavaId(entityId);
         }
 
-        if (entityMetadata.getId() == 16) {
-            metadata.put(EntityData.WITHER_TARGET_1, targetID);
-        } else if (entityMetadata.getId() == 17) {
-            metadata.put(EntityData.WITHER_TARGET_2, targetID);
-        } else if (entityMetadata.getId() == 18) {
-            metadata.put(EntityData.WITHER_TARGET_3, targetID);
-        } else if (entityMetadata.getId() == 19) {
-            metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, entityMetadata.getValue());
-
-            // Show the shield for the first few seconds of spawning (like Java)
-            if ((int) entityMetadata.getValue() >= 165) {
-                metadata.put(EntityData.WITHER_AERIAL_ATTACK, (short) 0);
-            } else {
-                metadata.put(EntityData.WITHER_AERIAL_ATTACK, (short) 1);
-            }
+        if (entity != null) {
+            dirtyMetadata.put(entityData, entity.getGeyserId());
         }
+    }
 
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setInvulnerableTicks(EntityMetadata<Integer> entityMetadata) {
+        int value = ((IntEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.WITHER_INVULNERABLE_TICKS, value);
+
+        // Show the shield for the first few seconds of spawning (like Java)
+        if (value >= 165) {
+            dirtyMetadata.put(EntityData.WITHER_AERIAL_ATTACK, (short) 0);
+        } else {
+            dirtyMetadata.put(EntityData.WITHER_AERIAL_ATTACK, (short) 1);
+        }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZoglinEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZoglinEntity.java
index dde19927d..4773b2d80 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZoglinEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZoglinEntity.java
@@ -26,27 +26,24 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class ZoglinEntity extends MonsterEntity {
 
-    public ZoglinEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ZoglinEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            boolean isBaby = (boolean) entityMetadata.getValue();
-            if (isBaby) {
-                metadata.put(EntityData.SCALE, .55f);
-                metadata.getFlags().setFlag(EntityFlag.BABY, true);
-            }
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setBaby(EntityMetadata<Boolean> entityMetadata) {
+        boolean isBaby = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.SCALE, isBaby? .55f : 1f);
+        setFlag(EntityFlag.BABY, isBaby);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieEntity.java
index bae593e4f..fd9218cd7 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieEntity.java
@@ -26,34 +26,35 @@
 package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class ZombieEntity extends MonsterEntity {
     private boolean convertingToDrowned = false;
 
-    public ZombieEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ZombieEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setZombieBaby(EntityMetadata<Boolean> entityMetadata) {
+        boolean isBaby = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        dirtyMetadata.put(EntityData.SCALE, isBaby ? .55f : 1.0f);
+        setFlag(EntityFlag.BABY, isBaby);
+    }
+
+    public void setConvertingToDrowned(EntityMetadata<Boolean> entityMetadata) {
+        convertingToDrowned = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.SHAKING, isShaking());
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 16) {
-            boolean isBaby = (boolean) entityMetadata.getValue();
-            metadata.put(EntityData.SCALE, isBaby ? .55f : 1.0f);
-            metadata.getFlags().setFlag(EntityFlag.BABY, isBaby);
-        } else if (entityMetadata.getId() == 18) {
-            convertingToDrowned = (boolean) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.SHAKING, isShaking(session));
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    protected boolean isShaking(GeyserSession session) {
-        return convertingToDrowned || super.isShaking(session);
+    protected boolean isShaking() {
+        return convertingToDrowned || super.isShaking();
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieVillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieVillagerEntity.java
index 2e3308ec1..744d3e559 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieVillagerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombieVillagerEntity.java
@@ -27,39 +27,39 @@ package org.geysermc.connector.entity.living.monster;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.merchant.VillagerEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class ZombieVillagerEntity extends ZombieEntity {
     private boolean isTransforming;
 
-    public ZombieVillagerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ZombieVillagerEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
+    }
+
+    public void setTransforming(EntityMetadata<Boolean> entityMetadata) {
+        isTransforming = ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.IS_TRANSFORMING, isTransforming);
+        setFlag(EntityFlag.SHAKING, isShaking());
+    }
+
+    public void setZombieVillagerData(EntityMetadata<VillagerData> entityMetadata) {
+        VillagerData villagerData = entityMetadata.getValue();
+        dirtyMetadata.put(EntityData.VARIANT, VillagerEntity.VILLAGER_PROFESSIONS.get(villagerData.getProfession())); // Actually works properly with the OptionalPack
+        dirtyMetadata.put(EntityData.MARK_VARIANT, VillagerEntity.VILLAGER_REGIONS.get(villagerData.getType()));
+        // Used with the OptionalPack
+        dirtyMetadata.put(EntityData.TRADE_TIER, villagerData.getLevel() - 1);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 19) {
-            isTransforming = (boolean) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.IS_TRANSFORMING, (boolean) entityMetadata.getValue());
-            metadata.getFlags().setFlag(EntityFlag.SHAKING, isShaking(session));
-        }
-        if (entityMetadata.getId() == 20) {
-            VillagerData villagerData = (VillagerData) entityMetadata.getValue();
-            metadata.put(EntityData.VARIANT, VillagerEntity.VILLAGER_PROFESSIONS.get(villagerData.getProfession())); // Actually works properly with the OptionalPack
-            metadata.put(EntityData.MARK_VARIANT, VillagerEntity.VILLAGER_REGIONS.get(villagerData.getType()));
-            // Used with the OptionalPack
-            metadata.put(EntityData.TRADE_TIER, villagerData.getLevel() - 1);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
-    }
-
-    @Override
-    protected boolean isShaking(GeyserSession session) {
-        return isTransforming || super.isShaking(session);
+    protected boolean isShaking() {
+        return isTransforming || super.isShaking();
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombifiedPiglinEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombifiedPiglinEntity.java
index ad00145bc..ddfd3184b 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombifiedPiglinEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/ZombifiedPiglinEntity.java
@@ -27,13 +27,16 @@ package org.geysermc.connector.entity.living.monster;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class ZombifiedPiglinEntity extends ZombieEntity {
 
-    public ZombifiedPiglinEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public ZombifiedPiglinEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
 
-        metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true);
+        setFlag(EntityFlag.FIRE_IMMUNE, true);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/AbstractIllagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/AbstractIllagerEntity.java
index e0fa18001..8350ae775 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/AbstractIllagerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/AbstractIllagerEntity.java
@@ -26,11 +26,14 @@
 package org.geysermc.connector.entity.living.monster.raid;
 
 import com.nukkitx.math.vector.Vector3f;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class AbstractIllagerEntity extends RaidParticipantEntity {
 
-    public AbstractIllagerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public AbstractIllagerEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/PillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/PillagerEntity.java
index 325cac7ab..d2cb661c6 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/PillagerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/PillagerEntity.java
@@ -27,26 +27,28 @@ package org.geysermc.connector.entity.living.monster.raid;
 
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
+import java.util.UUID;
+
 public class PillagerEntity extends AbstractIllagerEntity {
 
-    public PillagerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public PillagerEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateMainHand(GeyserSession session) {
-        checkForCrossbow(session);
+    public void updateMainHand(GeyserSession session) { //TODO
+        checkForCrossbow();
 
         super.updateMainHand(session);
     }
 
     @Override
     public void updateOffHand(GeyserSession session) {
-        checkForCrossbow(session);
+        checkForCrossbow();
 
         super.updateOffHand(session);
     }
@@ -54,15 +56,13 @@ public class PillagerEntity extends AbstractIllagerEntity {
     /**
      * Check for a crossbow in either the mainhand or offhand. If one exists, indicate that the pillager should be posing
      */
-    protected void checkForCrossbow(GeyserSession session) {
+    protected void checkForCrossbow() {
         ItemMapping crossbow = session.getItemMappings().getStoredItems().crossbow();
         boolean hasCrossbow = this.hand.getId() == crossbow.getBedrockId()
                 || this.offHand.getId() == crossbow.getBedrockId();
-        boolean usingItemChanged = metadata.getFlags().setFlag(EntityFlag.USING_ITEM, hasCrossbow);
-        boolean chargedChanged = metadata.getFlags().setFlag(EntityFlag.CHARGED, hasCrossbow);
+        setFlag(EntityFlag.USING_ITEM, hasCrossbow);
+        setFlag(EntityFlag.CHARGED, hasCrossbow);
 
-        if (usingItemChanged || chargedChanged) {
-            updateBedrockMetadata(session);
-        }
+        updateBedrockMetadata();
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/RaidParticipantEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/RaidParticipantEntity.java
index 15248f454..fcfd50425 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/RaidParticipantEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/RaidParticipantEntity.java
@@ -26,12 +26,15 @@
 package org.geysermc.connector.entity.living.monster.raid;
 
 import com.nukkitx.math.vector.Vector3f;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.entity.living.monster.MonsterEntity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.network.session.GeyserSession;
+
+import java.util.UUID;
 
 public class RaidParticipantEntity extends MonsterEntity {
 
-    public RaidParticipantEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public RaidParticipantEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/SpellcasterIllagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/SpellcasterIllagerEntity.java
index 693220dda..23485750d 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/SpellcasterIllagerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/SpellcasterIllagerEntity.java
@@ -26,38 +26,38 @@
 package org.geysermc.connector.entity.living.monster.raid;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class SpellcasterIllagerEntity extends AbstractIllagerEntity {
     private static final int SUMMON_VEX_PARTICLE_COLOR = (179 << 16) | (179 << 8) | 204;
     private static final int ATTACK_PARTICLE_COLOR = (102 << 16) | (77 << 8) | 89;
     private static final int WOLOLO_PARTICLE_COLOR = (179 << 16) | (128 << 8) | 51;
 
-    public SpellcasterIllagerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public SpellcasterIllagerEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
         // OptionalPack usage
-        metadata.getFlags().setFlag(EntityFlag.BRIBED, this.entityType == EntityType.ILLUSIONER);
+        dirtyMetadata.getFlags().setFlag(EntityFlag.BRIBED, this.definition == EntityDefinitions.ILLUSIONER);
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        if (entityMetadata.getId() == 17) {
-            int spellType = (int) (byte) entityMetadata.getValue();
-            // Summon vex, attack, or wololo
-            metadata.getFlags().setFlag(EntityFlag.CASTING, spellType == 1 || spellType == 2 || spellType == 3);
-            int rgbData = switch (spellType) {
-                // Set the spell color based on Java values
-                case 1 -> SUMMON_VEX_PARTICLE_COLOR;
-                case 2 -> ATTACK_PARTICLE_COLOR;
-                case 3 -> WOLOLO_PARTICLE_COLOR;
-                default -> 0;
-            };
-            metadata.put(EntityData.EVOKER_SPELL_COLOR, rgbData);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+    public void setSpellType(EntityMetadata<Byte> entityMetadata) {
+        int spellType = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        // Summon vex, attack, or wololo
+        setFlag(EntityFlag.CASTING, spellType == 1 || spellType == 2 || spellType == 3);
+        int rgbData = switch (spellType) {
+            // Set the spell color based on Java values
+            case 1 -> SUMMON_VEX_PARTICLE_COLOR;
+            case 2 -> ATTACK_PARTICLE_COLOR;
+            case 3 -> WOLOLO_PARTICLE_COLOR;
+            default -> 0;
+        };
+        dirtyMetadata.put(EntityData.EVOKER_SPELL_COLOR, rgbData);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/VindicatorEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/VindicatorEntity.java
index 4a9360393..74616806c 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/VindicatorEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/raid/VindicatorEntity.java
@@ -26,24 +26,25 @@
 package org.geysermc.connector.entity.living.monster.raid;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.UUID;
+
 public class VindicatorEntity extends AbstractIllagerEntity {
 
-    public VindicatorEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, entityType, position, motion, rotation);
+    public VindicatorEntity(GeyserSession session, long entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
+    public void setMobFlags(EntityMetadata<Byte> entityMetadata) {
+        super.setMobFlags(entityMetadata);
         // Allow the axe to be shown if necessary
-        if (entityMetadata.getId() == 15) {
-            byte xd = (byte) entityMetadata.getValue();
-            metadata.getFlags().setFlag(EntityFlag.ANGRY, (xd & 4) == 4);
-        }
-        super.updateBedrockMetadata(entityMetadata, session);
+        byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue();
+        setFlag(EntityFlag.ANGRY, (xd & 4) == 4);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java
index 5b948ef37..cfa78e08a 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java
@@ -28,6 +28,8 @@ package org.geysermc.connector.entity.player;
 import com.github.steveice10.mc.auth.data.GameProfile;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition;
 import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
@@ -45,9 +47,9 @@ import lombok.Setter;
 import net.kyori.adventure.text.Component;
 import org.geysermc.connector.common.ChatColor;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.LivingEntity;
 import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.chat.MessageTranslator;
 import org.geysermc.connector.scoreboard.Objective;
@@ -57,13 +59,11 @@ import org.geysermc.connector.scoreboard.UpdateType;
 
 import javax.annotation.Nullable;
 import java.util.Collections;
-import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 @Getter @Setter
 public class PlayerEntity extends LivingEntity {
     private GameProfile profile;
-    private UUID uuid;
     private String username;
     private boolean playerList = true;  // Player is in the player list
 
@@ -76,34 +76,37 @@ public class PlayerEntity extends LivingEntity {
      */
     private ParrotEntity rightParrot;
 
-    public PlayerEntity(GameProfile gameProfile, long entityId, long geyserId, Vector3f position, Vector3f motion, Vector3f rotation) {
-        super(entityId, geyserId, EntityType.PLAYER, position, motion, rotation);
+    public PlayerEntity(GeyserSession session, long entityId, long geyserId, GameProfile gameProfile, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
+        super(session, entityId, geyserId, gameProfile.getId(), EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw);
 
         profile = gameProfile;
-        uuid = gameProfile.getId();
         username = gameProfile.getName();
-
-        // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior
-        metadata.put(EntityData.MARK_VARIANT, 0xff);
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior
+        dirtyMetadata.put(EntityData.MARK_VARIANT, 0xff);
+    }
+
+    @Override
+    public void spawnEntity() {
         // Check to see if the player should have a belowname counterpart added
         Objective objective = session.getWorldCache().getScoreboard().getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
         if (objective != null) {
-            setBelowNameText(session, objective);
+            setBelowNameText(objective);
         }
 
         // The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
-        updateDisplayName(session, null, false);
+        updateDisplayName(null, false);
 
         AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
         addPlayerPacket.setUuid(uuid);
         addPlayerPacket.setUsername(username);
         addPlayerPacket.setRuntimeEntityId(geyserId);
         addPlayerPacket.setUniqueEntityId(geyserId);
-        addPlayerPacket.setPosition(position.sub(0, EntityType.PLAYER.getOffset(), 0));
+        addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0));
         addPlayerPacket.setRotation(getBedrockRotation());
         addPlayerPacket.setMotion(motion);
         addPlayerPacket.setHand(hand);
@@ -111,7 +114,11 @@ public class PlayerEntity extends LivingEntity {
         addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER);
         addPlayerPacket.setDeviceId("");
         addPlayerPacket.setPlatformChatId("");
-        addPlayerPacket.getMetadata().putAll(metadata);
+        addPlayerPacket.getMetadata().putAll(dirtyMetadata);
+        addPlayerPacket.getMetadata().putFlags(flags);
+
+        dirtyMetadata.clear();
+        setFlagsDirty(false);
 
         long linkedEntityId = session.getEntityCache().getCachedPlayerEntityLink(entityId);
         if (linkedEntityId != -1) {
@@ -125,21 +132,23 @@ public class PlayerEntity extends LivingEntity {
         session.sendUpstreamPacket(addPlayerPacket);
     }
 
-    public void sendPlayer(GeyserSession session) {
+    public void sendPlayer() {
         if (session.getEntityCache().getPlayerEntity(uuid) == null)
             return;
 
         if (session.getEntityCache().getEntityByGeyserId(geyserId) == null) {
             session.getEntityCache().spawnEntity(this);
         } else {
-            spawnEntity(session);
+            spawnEntity();
         }
     }
 
     @Override
-    public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
+    public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
         setPosition(position);
-        setRotation(rotation);
+        setYaw(yaw);
+        setPitch(pitch);
+        setHeadYaw(headYaw);
 
         setOnGround(isOnGround);
 
@@ -156,16 +165,18 @@ public class PlayerEntity extends LivingEntity {
 
         session.sendUpstreamPacket(movePlayerPacket);
         if (leftParrot != null) {
-            leftParrot.moveAbsolute(session, position, rotation, true, teleported);
+            leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported);
         }
         if (rightParrot != null) {
-            rightParrot.moveAbsolute(session, position, rotation, true, teleported);
+            rightParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported);
         }
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
-        setRotation(rotation);
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+        setYaw(yaw);
+        setPitch(pitch);
+        setHeadYaw(headYaw);
         this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
 
         setOnGround(isOnGround);
@@ -178,27 +189,27 @@ public class PlayerEntity extends LivingEntity {
         movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL);
         // If the player is moved while sleeping, we have to adjust their y, so it appears
         // correctly on Bedrock. This fixes GSit's lay.
-        if (metadata.getFlags().getFlag(EntityFlag.SLEEPING)) {
-            Vector3i bedPosition = metadata.getPos(EntityData.BED_POSITION);
+        if (dirtyMetadata.getFlags().getFlag(EntityFlag.SLEEPING)) {
+            Vector3i bedPosition = dirtyMetadata.getPos(EntityData.BED_POSITION);
             if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position.toInt()) > 4)) {
                 // Force the player movement by using a teleport
-                movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - entityType.getOffset() + 0.2f, position.getZ()));
+                movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ()));
                 movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
                 movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.UNKNOWN);
             }
         }
         session.sendUpstreamPacket(movePlayerPacket);
         if (leftParrot != null) {
-            leftParrot.moveRelative(session, relX, relY, relZ, rotation, true);
+            leftParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true);
         }
         if (rightParrot != null) {
-            rightParrot.moveRelative(session, relX, relY, relZ, rotation, true);
+            rightParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true);
         }
     }
 
     @Override
-    public void updateHeadLookRotation(GeyserSession session, float headYaw) {
-        moveRelative(session, 0, 0, 0, Vector3f.from(rotation.getX(), rotation.getY(), headYaw), onGround);
+    public void updateHeadLookRotation(float headYaw) {
+        moveRelative(0, 0, 0, yaw, pitch, headYaw, onGround);
         MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
         movePlayerPacket.setRuntimeEntityId(geyserId);
         movePlayerPacket.setPosition(position);
@@ -208,19 +219,19 @@ public class PlayerEntity extends LivingEntity {
     }
 
     @Override
-    public void updatePositionAndRotation(GeyserSession session, double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) {
-        moveRelative(session, moveX, moveY, moveZ, yaw, pitch, isOnGround);
+    public void updatePositionAndRotation(double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) {
+        moveRelative(moveX, moveY, moveZ, yaw, pitch, isOnGround);
         if (leftParrot != null) {
-            leftParrot.moveRelative(session, moveX, moveY, moveZ, yaw, pitch, isOnGround);
+            leftParrot.moveRelative(moveX, moveY, moveZ, yaw, pitch, isOnGround);
         }
         if (rightParrot != null) {
-            rightParrot.moveRelative(session, moveX, moveY, moveZ, yaw, pitch, isOnGround);
+            rightParrot.moveRelative(moveX, moveY, moveZ, yaw, pitch, isOnGround);
         }
     }
 
     @Override
-    public void updateRotation(GeyserSession session, float yaw, float pitch, boolean isOnGround) {
-        super.updateRotation(session, yaw, pitch, isOnGround);
+    public void updateRotation(float yaw, float pitch, boolean isOnGround) {
+        super.updateRotation(yaw, pitch, isOnGround);
         // Both packets need to be sent or else player head rotation isn't correctly updated
         MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
         movePlayerPacket.setRuntimeEntityId(geyserId);
@@ -230,86 +241,90 @@ public class PlayerEntity extends LivingEntity {
         movePlayerPacket.setMode(MovePlayerPacket.Mode.HEAD_ROTATION);
         session.sendUpstreamPacket(movePlayerPacket);
         if (leftParrot != null) {
-            leftParrot.updateRotation(session, yaw, pitch, isOnGround);
+            leftParrot.updateRotation(yaw, pitch, isOnGround);
         }
         if (rightParrot != null) {
-            rightParrot.updateRotation(session, yaw, pitch, isOnGround);
+            rightParrot.updateRotation(yaw, pitch, isOnGround);
         }
     }
 
     @Override
     public void setPosition(Vector3f position) {
-        super.setPosition(position.add(0, entityType.getOffset(), 0));
+        super.setPosition(position.add(0, definition.offset(), 0));
     }
 
-    @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-
+    public void setAbsorptionHearts(EntityMetadata<Float> entityMetadata) {
         // Extra hearts - is not metadata but an attribute on Bedrock
-        if (entityMetadata.getId() == 15) {
-            UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
-            attributesPacket.setRuntimeEntityId(geyserId);
-            // Setting to a higher maximum since plugins/datapacks can probably extend the Bedrock soft limit
-            attributesPacket.setAttributes(Collections.singletonList(
-                    new AttributeData("minecraft:absorption", 0.0f, 1024f, (float) entityMetadata.getValue(), 0.0f)));
-            session.sendUpstreamPacket(attributesPacket);
-        }
+        UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
+        attributesPacket.setRuntimeEntityId(geyserId);
+        // Setting to a higher maximum since plugins/datapacks can probably extend the Bedrock soft limit
+        attributesPacket.setAttributes(Collections.singletonList(
+                new AttributeData("minecraft:absorption", 0.0f, 1024f, ((FloatEntityMetadata) entityMetadata).getPrimitiveValue(), 0.0f)));
+        session.sendUpstreamPacket(attributesPacket);
+    }
 
-        if (entityMetadata.getId() == 17) {
-            // OptionalPack usage for toggling skin bits
-            // In Java Edition, a bit being set means that part should be enabled
-            // However, to ensure that the pack still works on other servers, we invert the bit so all values by default
-            // are true (0).
-            metadata.put(EntityData.MARK_VARIANT, ~((byte) entityMetadata.getValue()) & 0xff);
-        }
+    public void setSkinVisibility(EntityMetadata<Byte> entityMetadata) {
+        // OptionalPack usage for toggling skin bits
+        // In Java Edition, a bit being set means that part should be enabled
+        // However, to ensure that the pack still works on other servers, we invert the bit so all values by default
+        // are true (0).
+        dirtyMetadata.put(EntityData.MARK_VARIANT, ~((ByteEntityMetadata) entityMetadata).getPrimitiveValue() & 0xff);
+    }
 
-        // Parrot occupying shoulder
-        if (entityMetadata.getId() == 19 || entityMetadata.getId() == 20) {
-            CompoundTag tag = (CompoundTag) entityMetadata.getValue();
-            boolean isLeft = entityMetadata.getId() == 19;
-            if (tag != null && !tag.isEmpty()) {
-                if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) {
-                    // No need to update a parrot's data when it already exists
-                    return;
-                }
-                // The parrot is a separate entity in Bedrock, but part of the player entity in Java
-                ParrotEntity parrot = new ParrotEntity(0, session.getEntityCache().getNextEntityId().incrementAndGet(),
-                        EntityType.PARROT, position, motion, rotation);
-                parrot.spawnEntity(session);
-                parrot.getMetadata().put(EntityData.VARIANT, tag.get("Variant").getValue());
-                // Different position whether the parrot is left or right
-                float offset = isLeft ? 0.4f : -0.4f;
-                parrot.getMetadata().put(EntityData.RIDER_SEAT_POSITION, Vector3f.from(offset, -0.22, -0.1));
-                parrot.getMetadata().put(EntityData.RIDER_ROTATION_LOCKED, 1);
-                parrot.updateBedrockMetadata(session);
-                SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
-                EntityLinkData.Type type = isLeft ? EntityLinkData.Type.RIDER : EntityLinkData.Type.PASSENGER;
-                linkPacket.setEntityLink(new EntityLinkData(geyserId, parrot.getGeyserId(), type, false, false));
-                // Delay, or else spawned-in players won't get the link
-                // TODO: Find a better solution. This problem also exists with item frames
-                session.scheduleInEventLoop(() -> session.sendUpstreamPacket(linkPacket), 500, TimeUnit.MILLISECONDS);
-                if (isLeft) {
-                    leftParrot = parrot;
-                } else {
-                    rightParrot = parrot;
-                }
+    public void setLeftParrot(EntityMetadata<CompoundTag> entityMetadata) {
+        setParrot(entityMetadata.getValue(), true);
+    }
+
+    public void setRightParrot(EntityMetadata<CompoundTag> entityMetadata) {
+        setParrot(entityMetadata.getValue(), false);
+    }
+
+    /**
+     * Sets the parrot occupying the shoulder. Bedrock Edition requires a full entity whereas Java Edition just
+     * spawns it from the NBT data provided
+     */
+    private void setParrot(CompoundTag tag, boolean isLeft) {
+        if (tag != null && !tag.isEmpty()) {
+            if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) {
+                // No need to update a parrot's data when it already exists
+                return;
+            }
+            // The parrot is a separate entity in Bedrock, but part of the player entity in Java //TODO is a UUID provided in NBT?
+            ParrotEntity parrot = new ParrotEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(),
+                    null, EntityDefinitions.PARROT, position, motion, yaw, pitch, headYaw);
+            parrot.spawnEntity();
+            parrot.getDirtyMetadata().put(EntityData.VARIANT, tag.get("Variant").getValue());
+            // Different position whether the parrot is left or right
+            float offset = isLeft ? 0.4f : -0.4f;
+            parrot.getDirtyMetadata().put(EntityData.RIDER_SEAT_POSITION, Vector3f.from(offset, -0.22, -0.1));
+            parrot.getDirtyMetadata().put(EntityData.RIDER_ROTATION_LOCKED, 1);
+            parrot.updateBedrockMetadata();
+            SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
+            EntityLinkData.Type type = isLeft ? EntityLinkData.Type.RIDER : EntityLinkData.Type.PASSENGER;
+            linkPacket.setEntityLink(new EntityLinkData(geyserId, parrot.getGeyserId(), type, false, false));
+            // Delay, or else spawned-in players won't get the link
+            // TODO: Find a better solution.
+            session.scheduleInEventLoop(() -> session.sendUpstreamPacket(linkPacket), 500, TimeUnit.MILLISECONDS);
+            if (isLeft) {
+                leftParrot = parrot;
             } else {
-                Entity parrot = isLeft ? leftParrot : rightParrot;
-                if (parrot != null) {
-                    parrot.despawnEntity(session);
-                    if (isLeft) {
-                        leftParrot = null;
-                    } else {
-                        rightParrot = null;
-                    }
+                rightParrot = parrot;
+            }
+        } else {
+            Entity parrot = isLeft ? leftParrot : rightParrot;
+            if (parrot != null) {
+                parrot.despawnEntity();
+                if (isLeft) {
+                    leftParrot = null;
+                } else {
+                    rightParrot = null;
                 }
             }
         }
     }
 
     @Override
-    protected void setDisplayName(GeyserSession session, Component name) {
+    public void setDisplayName(EntityMetadata<Component> entityMetadata) {
         // Doesn't do anything for players
     }
 
@@ -317,7 +332,7 @@ public class PlayerEntity extends LivingEntity {
     /**
      * @param useGivenTeam even if there is no team, update the username in the entity metadata anyway, and don't look for a team
      */
-    public void updateDisplayName(GeyserSession session, @Nullable Team team, boolean useGivenTeam) {
+    public void updateDisplayName(@Nullable Team team, boolean useGivenTeam) {
         if (team == null && !useGivenTeam) {
             // Only search for the team if we are not supposed to use the given team
             // If the given team is null, this is intentional that we are being removed from the team
@@ -343,12 +358,12 @@ public class PlayerEntity extends LivingEntity {
                 // The name is not visible to the session player; clear name
                 newDisplayName = "";
             }
-            needsUpdate = useGivenTeam && !newDisplayName.equals(metadata.getString(EntityData.NAMETAG, null));
-            metadata.put(EntityData.NAMETAG, newDisplayName);
+            needsUpdate = useGivenTeam && !newDisplayName.equals(dirtyMetadata.getString(EntityData.NAMETAG, null));
+            dirtyMetadata.put(EntityData.NAMETAG, newDisplayName);
         } else if (useGivenTeam) {
             // The name has reset, if it was previously something else
-            needsUpdate = !newDisplayName.equals(metadata.getString(EntityData.NAMETAG));
-            metadata.put(EntityData.NAMETAG, this.username);
+            needsUpdate = !newDisplayName.equals(dirtyMetadata.getString(EntityData.NAMETAG));
+            dirtyMetadata.put(EntityData.NAMETAG, this.username);
         } else {
             needsUpdate = false;
         }
@@ -363,7 +378,7 @@ public class PlayerEntity extends LivingEntity {
     }
 
     @Override
-    protected void setDisplayNameVisible(EntityMetadata entityMetadata) {
+    public void setDisplayNameVisible(EntityMetadata<Boolean> entityMetadata) {
         // Doesn't do anything for players
     }
 
@@ -378,11 +393,13 @@ public class PlayerEntity extends LivingEntity {
                 return;
             }
         }
-        metadata.put(EntityData.BOUNDING_BOX_WIDTH, entityType.getWidth());
-        metadata.put(EntityData.BOUNDING_BOX_HEIGHT, height);
+        if (height != boundingBoxHeight || definition.width() != boundingBoxWidth) {
+            dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, definition.width());
+            dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, height);
+        }
     }
 
-    public void setBelowNameText(GeyserSession session, Objective objective) {
+    public void setBelowNameText(Objective objective) {
         if (objective != null && objective.getUpdateType() != UpdateType.REMOVE) {
             int amount;
             Score score = objective.getScores().get(username);
@@ -393,7 +410,7 @@ public class PlayerEntity extends LivingEntity {
             }
             String displayString = amount + " " + objective.getDisplayName();
 
-            metadata.put(EntityData.SCORE_TAG, displayString);
+            dirtyMetadata.put(EntityData.SCORE_TAG, displayString);
             if (valid) {
                 // Already spawned - we still need to run the rest of this code because the spawn packet will be
                 // providing the information
@@ -405,7 +422,7 @@ public class PlayerEntity extends LivingEntity {
         } else {
             // Always remove the score tag first, then check for valid.
             // That way the score tag is removed if the player was spawned, then despawned, and is being respawned
-            if (metadata.remove(EntityData.SCORE_TAG) != null && valid) {
+            if (dirtyMetadata.remove(EntityData.SCORE_TAG) != null && valid) {
                 SetEntityDataPacket packet = new SetEntityDataPacket();
                 packet.setRuntimeEntityId(geyserId);
                 packet.getMetadata().put(EntityData.SCORE_TAG, "");
diff --git a/connector/src/main/java/org/geysermc/connector/entity/player/SessionPlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/player/SessionPlayerEntity.java
index 373ff6f43..d8a3e5492 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/player/SessionPlayerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/player/SessionPlayerEntity.java
@@ -30,6 +30,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.attribute.Attribute;
 import com.github.steveice10.mc.protocol.data.game.entity.attribute.AttributeType;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.AttributeData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
@@ -62,21 +63,21 @@ public class SessionPlayerEntity extends PlayerEntity {
     private final GeyserSession session;
 
     public SessionPlayerEntity(GeyserSession session) {
-        super(new GameProfile(UUID.randomUUID(), "unknown"), 1, 1, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO);
+        super(session, 1, 1, new GameProfile(UUID.randomUUID(), "unknown"), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0);
 
         valid = true;
         this.session = session;
     }
 
     @Override
-    public void spawnEntity(GeyserSession session) {
+    public void spawnEntity() {
         // Already logged in
     }
 
     @Override
-    public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
-        super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
-        session.getCollisionManager().updatePlayerBoundingBox(this.position.down(entityType.getOffset()));
+    public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+        super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
+        session.getCollisionManager().updatePlayerBoundingBox(this.position.down(definition.offset()));
     }
 
     @Override
@@ -99,24 +100,25 @@ public class SessionPlayerEntity extends PlayerEntity {
     }
 
     @Override
-    public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
-        super.updateBedrockMetadata(entityMetadata, session);
-        if (entityMetadata.getId() == 0) {
-            session.setSwimmingInWater((((byte) entityMetadata.getValue()) & 0x10) == 0x10 && metadata.getFlags().getFlag(EntityFlag.SPRINTING));
-            refreshSpeed = true;
-        } else if (entityMetadata.getId() == 6) {
-            session.setPose((Pose) entityMetadata.getValue());
-            refreshSpeed = true;
-        }
+    public void setFlags(EntityMetadata<Byte> entityMetadata) {
+        super.setFlags(entityMetadata);
+        session.setSwimmingInWater((((ByteEntityMetadata) entityMetadata).getPrimitiveValue() & 0x10) == 0x10 && getFlag(EntityFlag.SPRINTING));
+        refreshSpeed = true;
+    }
+
+    @Override
+    public void setPose(EntityMetadata<Pose> entityMetadata) {
+        super.setPose(entityMetadata);
+        session.setPose(entityMetadata.getValue());
+        refreshSpeed = true;
     }
 
     public float getMaxHealth() {
         return maxHealth;
     }
 
-    @Override
     public void setHealth(float health) {
-        super.setHealth(health);
+        this.health = health;
     }
 
     @Override
@@ -138,8 +140,8 @@ public class SessionPlayerEntity extends PlayerEntity {
     }
 
     @Override
-    public void updateBedrockMetadata(GeyserSession session) {
-        super.updateBedrockMetadata(session);
+    public void updateBedrockMetadata() {
+        super.updateBedrockMetadata();
         if (refreshSpeed) {
             AttributeData speedAttribute = session.adjustSpeed();
             if (speedAttribute != null) {
diff --git a/connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java
index 61b5af38e..c2c41d0dc 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/player/SkullPlayerEntity.java
@@ -34,7 +34,6 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket;
 import lombok.Getter;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
 /**
@@ -49,31 +48,34 @@ public class SkullPlayerEntity extends PlayerEntity {
     @Getter
     private final int blockState;
 
-    public SkullPlayerEntity(GameProfile gameProfile, long geyserId, Vector3f position, Vector3f rotation, int blockState) {
-        super(gameProfile, 0, geyserId, position, Vector3f.ZERO, rotation);
+    public SkullPlayerEntity(GeyserSession session, long geyserId, GameProfile gameProfile, Vector3f position, float rotation, int blockState) {
+        super(session, 0, geyserId, gameProfile, position, Vector3f.ZERO, rotation, 0, rotation);
         this.blockState = blockState;
         setPlayerList(false);
+    }
 
-        //Set bounding box to almost nothing so the skull is able to be broken and not cause entity to cast a shadow
-        metadata.clear();
-        metadata.put(EntityData.SCALE, 1.08f);
-        metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.001f);
-        metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.001f);
-        metadata.getOrCreateFlags().setFlag(EntityFlag.CAN_SHOW_NAME, false);
-        metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded
+    @Override
+    protected void initializeMetadata() {
+        // Deliberately do not call super
+        // Set bounding box to almost nothing so the skull is able to be broken and not cause entity to cast a shadow
+        dirtyMetadata.put(EntityData.SCALE, 1.08f);
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.001f);
+        dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.001f);
+        setFlag(EntityFlag.CAN_SHOW_NAME, false);
+        setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded
     }
 
     /**
      * Overwritten so each entity doesn't check for a linked entity
      */
     @Override
-    public void spawnEntity(GeyserSession session) {
+    public void spawnEntity() {
         AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
         addPlayerPacket.setUuid(getUuid());
         addPlayerPacket.setUsername(getUsername());
         addPlayerPacket.setRuntimeEntityId(geyserId);
         addPlayerPacket.setUniqueEntityId(geyserId);
-        addPlayerPacket.setPosition(position.sub(0, EntityType.PLAYER.getOffset(), 0));
+        addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0));
         addPlayerPacket.setRotation(getBedrockRotation());
         addPlayerPacket.setMotion(motion);
         addPlayerPacket.setHand(hand);
@@ -81,14 +83,18 @@ public class SkullPlayerEntity extends PlayerEntity {
         addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER);
         addPlayerPacket.setDeviceId("");
         addPlayerPacket.setPlatformChatId("");
-        addPlayerPacket.getMetadata().putAll(metadata);
+        addPlayerPacket.getMetadata().putAll(dirtyMetadata);
+        addPlayerPacket.getMetadata().putFlags(flags);
+
+        dirtyMetadata.clear();
+        setFlagsDirty(false);
 
         valid = true;
         session.sendUpstreamPacket(addPlayerPacket);
     }
 
-    public void despawnEntity(GeyserSession session, Vector3i position) {
-        this.despawnEntity(session);
+    public void despawnEntity(Vector3i position) {
+        this.despawnEntity();
         session.getSkullCache().remove(position, this);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java
index 85f6fde5b..517864e14 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java
@@ -26,222 +26,8 @@
 package org.geysermc.connector.entity.type;
 
 import lombok.Getter;
-import org.geysermc.connector.entity.*;
-import org.geysermc.connector.entity.living.*;
-import org.geysermc.connector.entity.living.animal.*;
-import org.geysermc.connector.entity.living.animal.horse.*;
-import org.geysermc.connector.entity.living.animal.tameable.CatEntity;
-import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity;
-import org.geysermc.connector.entity.living.animal.tameable.WolfEntity;
-import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity;
-import org.geysermc.connector.entity.living.merchant.VillagerEntity;
-import org.geysermc.connector.entity.living.monster.*;
-import org.geysermc.connector.entity.living.monster.raid.*;
-import org.geysermc.connector.entity.player.PlayerEntity;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
 
 @Getter
 public enum EntityType {
 
-    CHICKEN(ChickenEntity.class, 10, 0.7f, 0.4f),
-    COW(AnimalEntity.class, 11, 1.4f, 0.9f),
-    PIG(PigEntity.class, 12, 0.9f),
-    SHEEP(SheepEntity.class, 13, 1.3f, 0.9f),
-    WOLF(WolfEntity.class, 14, 0.85f, 0.6f),
-    VILLAGER(VillagerEntity.class, 15, 1.8f, 0.6f, 0.6f, 1.62f, "minecraft:villager_v2"),
-    MOOSHROOM(MooshroomEntity.class, 16, 1.4f, 0.9f),
-    SQUID(SquidEntity.class, 17, 0.8f),
-    RABBIT(RabbitEntity.class, 18, 0.5f, 0.4f),
-    BAT(BatEntity.class, 19, 0.9f, 0.5f),
-    IRON_GOLEM(IronGolemEntity.class, 20, 2.7f, 1.4f),
-    SNOW_GOLEM(SnowGolemEntity.class, 21, 1.9f, 0.7f),
-    OCELOT(OcelotEntity.class, 22, 0.35f, 0.3f),
-    HORSE(HorseEntity.class, 23, 1.6f, 1.3965f),
-    DONKEY(ChestedHorseEntity.class, 24, 1.6f, 1.3965f),
-    MULE(ChestedHorseEntity.class, 25, 1.6f, 1.3965f),
-    SKELETON_HORSE(AbstractHorseEntity.class, 26, 1.6f, 1.3965f),
-    ZOMBIE_HORSE(AbstractHorseEntity.class, 27, 1.6f, 1.3965f),
-    POLAR_BEAR(PolarBearEntity.class, 28, 1.4f, 1.3f),
-    LLAMA(LlamaEntity.class, 29, 1.87f, 0.9f),
-    TRADER_LLAMA(TraderLlamaEntity.class, 29, 1.187f, 0.9f, 0f, 0f, "minecraft:llama"),
-    PARROT(ParrotEntity.class, 30, 0.9f, 0.5f),
-    DOLPHIN(WaterEntity.class, 31, 0.6f, 0.9f),
-    ZOMBIE(ZombieEntity.class, 32, 1.8f, 0.6f, 0.6f, 1.62f),
-    GIANT(GiantEntity.class, 32, 1.8f, 0.6f, 0.6f, 1.62f, "minecraft:zombie"),
-    CREEPER(CreeperEntity.class, 33, 1.7f, 0.6f, 0.6f, 1.62f),
-    SKELETON(SkeletonEntity.class, 34, 1.8f, 0.6f, 0.6f, 1.62f),
-    SPIDER(SpiderEntity.class, 35, 0.9f, 1.4f, 1.4f, 1f),
-    ZOMBIFIED_PIGLIN(ZombifiedPiglinEntity.class, 36, 1.95f, 0.6f, 0.6f, 1.62f, "minecraft:zombie_pigman"),
-    SLIME(SlimeEntity.class, 37, 0.51f),
-    ENDERMAN(EndermanEntity.class, 38, 2.9f, 0.6f),
-    SILVERFISH(MonsterEntity.class, 39, 0.3f, 0.4f),
-    CAVE_SPIDER(MonsterEntity.class, 40, 0.5f, 0.7f),
-    GHAST(GhastEntity.class, 41, 4.0f),
-    MAGMA_CUBE(MagmaCubeEntity.class, 42, 0.51f),
-    BLAZE(BlazeEntity.class, 43, 1.8f, 0.6f),
-    ZOMBIE_VILLAGER(ZombieVillagerEntity.class, 44, 1.8f, 0.6f, 0.6f, 1.62f, "minecraft:zombie_villager_v2"),
-    WITCH(RaidParticipantEntity.class, 45, 1.8f, 0.6f, 0.6f, 1.62f),
-    STRAY(AbstractSkeletonEntity.class, 46, 1.8f, 0.6f, 0.6f, 1.62f),
-    HUSK(ZombieEntity.class, 47, 1.8f, 0.6f, 0.6f, 1.62f),
-    WITHER_SKELETON(AbstractSkeletonEntity.class, 48, 2.4f, 0.7f),
-    GUARDIAN(GuardianEntity.class, 49, 0.85f),
-    ELDER_GUARDIAN(ElderGuardianEntity.class, 50, 1.9975f),
-    NPC(PlayerEntity.class, 51, 1.8f, 0.6f, 0.6f, 1.62f),
-    WITHER(WitherEntity.class, 52, 3.5f, 0.9f),
-    ENDER_DRAGON(EnderDragonEntity.class, 53, 0f, 0f),
-    SHULKER(ShulkerEntity.class, 54, 1f, 1f),
-    ENDERMITE(MonsterEntity.class, 55, 0.3f, 0.4f),
-    AGENT(Entity.class, 56, 0f),
-    VINDICATOR(VindicatorEntity.class, 57, 1.8f, 0.6f, 0.6f, 1.62f),
-    PILLAGER(PillagerEntity.class, 114, 1.8f, 0.6f, 0.6f, 1.62f),
-    WANDERING_TRADER(AbstractMerchantEntity.class, 118, 1.8f, 0.6f, 0.6f, 1.62f),
-    PHANTOM(PhantomEntity.class, 58, 0.5f, 0.9f, 0.9f, 0.6f),
-    RAVAGER(RaidParticipantEntity.class, 59, 1.9f, 1.2f),
-
-    ARMOR_STAND(ArmorStandEntity.class, 61, 1.975f, 0.5f),
-    TRIPOD_CAMERA(Entity.class, 62, 0f),
-    PLAYER(PlayerEntity.class, 63, 1.8f, 0.6f, 0.6f, 1.62f),
-    ITEM(ItemEntity.class, 64, 0.25f, 0.25f, 0.25f, 0.125f),
-    PRIMED_TNT(TNTEntity.class, 65, 0.98f, 0.98f, 0.98f, 0f, "minecraft:tnt"),
-    FALLING_BLOCK(FallingBlockEntity.class, 66, 0.98f, 0.98f),
-    MOVING_BLOCK(Entity.class, 67, 0f),
-    THROWN_EXP_BOTTLE(ThrowableItemEntity.class, 68, 0.25f, 0.25f, 0f, 0f, "minecraft:xp_bottle"),
-    EXPERIENCE_ORB(ExpOrbEntity.class, 69, 0f, 0f, 0f, 0f, "minecraft:xp_orb"),
-    EYE_OF_ENDER(Entity.class, 70, 0.25f, 0.25f, 0f, 0f, "minecraft:eye_of_ender_signal"),
-    END_CRYSTAL(EnderCrystalEntity.class, 71, 2.0f, 2.0f, 2.0f, 0f, "minecraft:ender_crystal"),
-    FIREWORK_ROCKET(FireworkEntity.class, 72, 0.25f, 0.25f, 0.25f, 0f, "minecraft:fireworks_rocket"),
-    TRIDENT(TridentEntity.class, 73, 0f, 0f, 0f, 0f, "minecraft:thrown_trident"),
-    TURTLE(TurtleEntity.class, 74, 0.4f, 1.2f),
-    CAT(CatEntity.class, 75, 0.35f, 0.3f),
-    SHULKER_BULLET(ThrowableEntity.class, 76, 0.3125f),
-    FISHING_BOBBER(FishingHookEntity.class, 77, 0f, 0f, 0f, 0f, "minecraft:fishing_hook"),
-    CHALKBOARD(Entity.class, 78, 0f),
-    DRAGON_FIREBALL(ItemedFireballEntity.class, 79, 1.0f),
-    ARROW(TippedArrowEntity.class, 80, 0.25f, 0.25f),
-    SPECTRAL_ARROW(AbstractArrowEntity.class, 80, 0.25f, 0.25f, 0.25f, 0f, "minecraft:arrow"),
-    SNOWBALL(ThrowableItemEntity.class, 81, 0.25f),
-    THROWN_EGG(ThrowableItemEntity.class, 82, 0.25f, 0.25f, 0.25f, 0f, "minecraft:egg"),
-    PAINTING(PaintingEntity.class, 83, 0f),
-    MINECART(MinecartEntity.class, 84, 0.7f, 0.98f, 0.98f, 0.35f),
-    FIREBALL(ItemedFireballEntity.class, 85, 1.0f),
-    THROWN_POTION(ThrownPotionEntity.class, 86, 0.25f, 0.25f, 0.25f, 0f, "minecraft:splash_potion"),
-    THROWN_ENDERPEARL(ThrowableItemEntity.class, 87, 0.25f, 0.25f, 0.25f, 0f, "minecraft:ender_pearl"),
-    LEASH_KNOT(LeashKnotEntity.class, 88, 0.5f, 0.375f),
-    WITHER_SKULL(WitherSkullEntity.class, 89, 0.3125f),
-    BOAT(BoatEntity.class, 90, 0.6f, 1.6f, 1.6f, 0.35f),
-    WITHER_SKULL_DANGEROUS(WitherSkullEntity.class, 91, 0f),
-    LIGHTNING_BOLT(LightningEntity.class, 93, 0f),
-    SMALL_FIREBALL(ItemedFireballEntity.class, 94, 0.3125f),
-    AREA_EFFECT_CLOUD(AreaEffectCloudEntity.class, 95, 0.5f, 1.0f),
-    MINECART_HOPPER(MinecartEntity.class, 96, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:hopper_minecart"),
-    MINECART_TNT(MinecartEntity.class, 97, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:tnt_minecart"),
-    MINECART_CHEST(MinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:chest_minecart"),
-    MINECART_FURNACE(FurnaceMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"),
-    MINECART_SPAWNER(SpawnerMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"),
-    MINECART_COMMAND_BLOCK(CommandBlockMinecartEntity.class, 100, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:command_block_minecart"),
-    LINGERING_POTION(ThrowableEntity.class, 101, 0f),
-    LLAMA_SPIT(ThrowableEntity.class, 102, 0.25f),
-    EVOKER_FANGS(Entity.class, 103, 0.8f, 0.5f, 0.5f, 0f, "minecraft:evocation_fang"),
-    EVOKER(SpellcasterIllagerEntity.class, 104, 1.95f, 0.6f, 0.6f, 0f, "minecraft:evocation_illager"),
-    VEX(VexEntity.class, 105, 0.8f, 0.4f),
-    ICE_BOMB(Entity.class, 106, 0f),
-    BALLOON(Entity.class, 107, 0f),
-    PUFFERFISH(PufferFishEntity.class, 108, 0.7f, 0.7f),
-    SALMON(AbstractFishEntity.class, 109, 0.5f, 0.7f),
-    DROWNED(ZombieEntity.class, 110, 1.95f, 0.6f),
-    TROPICAL_FISH(TropicalFishEntity.class, 111, 0.6f, 0.6f, 0f, 0f, "minecraft:tropicalfish"),
-    COD(AbstractFishEntity.class, 112, 0.25f, 0.5f),
-    PANDA(PandaEntity.class, 113, 1.25f, 1.125f, 1.825f),
-    FOX(FoxEntity.class, 121, 0.5f, 1.25f),
-    BEE(BeeEntity.class, 122, 0.6f, 0.6f),
-    STRIDER(StriderEntity.class, 125, 1.7f, 0.9f, 0f, 0f, "minecraft:strider"),
-    HOGLIN(HoglinEntity.class, 124, 1.4f, 1.3965f, 1.3965f, 0f, "minecraft:hoglin"),
-    ZOGLIN(ZoglinEntity.class, 126, 1.4f, 1.3965f, 1.3965f, 0f, "minecraft:zoglin"),
-    PIGLIN(PiglinEntity.class, 123, 1.95f, 0.6f, 0.6f, 0f, "minecraft:piglin"),
-    PIGLIN_BRUTE(BasePiglinEntity.class, 127, 1.95f, 0.6f, 0.6f, 0f, "minecraft:piglin_brute"),
-    AXOLOTL(AxolotlEntity.class, 0, 0.42f, 0.7f, 0.7f, 0f, "minecraft:axolotl"),
-    GLOW_SQUID(GlowSquidEntity.class, 0, 0.8f, 0.8f, 0.8f, 0f, "minecraft:glow_squid"),
-    GOAT(GoatEntity.class, 0, 1.3f, 0.9f, 0.9f, 0f, "minecraft:goat"),
-    MARKER(Entity.class, 0, 0, 0, 0, 0, "minecraft:marker"), // Only should be used for ALL_JAVA_IDENTIFIERS
-
-    /**
-     * Item frames are handled differently since they are a block in Bedrock.
-     */
-    ITEM_FRAME(ItemFrameEntity.class, 0, 0, 0),
-    GLOW_ITEM_FRAME(ItemFrameEntity.class, 0, 0, 0),
-
-    /**
-     * Not an entity in Bedrock, so we replace it with an evoker
-     */
-    ILLUSIONER(SpellcasterIllagerEntity.class, 104, 1.8f, 0.6f, 0.6f, 1.62f, "minecraft:evocation_illager"),
-
-    /**
-     * Not an entity in Bedrock, but used for the Ender Dragon's multiple hitboxes
-     */
-    ENDER_DRAGON_PART(EnderDragonPartEntity.class, 32, 0, 0, 0, 0, "minecraft:armor_stand");
-
-    /**
-     * A list of all Java identifiers for use with command suggestions
-     */
-    public static final String[] ALL_JAVA_IDENTIFIERS;
-    private static final EntityType[] VALUES = values();
-
-    static {
-        List<String> allJavaIdentifiers = new ArrayList<>();
-        for (EntityType type : VALUES) {
-            if (type == AGENT || type == BALLOON || type == CHALKBOARD || type == NPC || type == TRIPOD_CAMERA || type == ENDER_DRAGON_PART) {
-                continue;
-            }
-            allJavaIdentifiers.add("minecraft:" + type.name().toLowerCase(Locale.ROOT));
-        }
-        ALL_JAVA_IDENTIFIERS = allJavaIdentifiers.toArray(new String[0]);
-    }
-
-    private final Class<? extends Entity> entityClass;
-    private final int type;
-    private final float height;
-    private final float width;
-    private final float length;
-    private final float offset;
-    private final String identifier;
-
-    EntityType(Class<? extends Entity> entityClass, int type, float height) {
-        //noinspection SuspiciousNameCombination
-        this(entityClass, type, height, height);
-    }
-
-    EntityType(Class<? extends Entity> entityClass, int type, float height, float width) {
-        this(entityClass, type, height, width, width);
-    }
-
-    EntityType(Class<? extends Entity> entityClass, int type, float height, float width, float length) {
-        this(entityClass, type, height, width, length, 0f);
-    }
-
-    EntityType(Class<? extends Entity> entityClass, int type, float height, float width, float length, float offset) {
-        this(entityClass, type, height, width, length, offset, null);
-    }
-
-    EntityType(Class<? extends Entity> entityClass, int type, float height, float width, float length, float offset, String identifier) {
-        this.entityClass = entityClass;
-        this.type = type;
-        this.height = height;
-        this.width = width;
-        this.length = length;
-        this.offset = offset + 0.00001f;
-        this.identifier = identifier == null ? "minecraft:" + name().toLowerCase() : identifier;
-    }
-
-    public static EntityType getFromIdentifier(String identifier) {
-        for (EntityType type : VALUES) {
-            if (type.identifier.equals(identifier)) {
-                return type;
-            }
-        }
-
-        return null;
-    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
index 33df29f9f..ef545746b 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
@@ -532,7 +532,7 @@ public class GeyserSession implements CommandSender {
         upstream.sendPacket(biomeDefinitionListPacket);
 
         AvailableEntityIdentifiersPacket entityPacket = new AvailableEntityIdentifiersPacket();
-        entityPacket.setIdentifiers(Registries.ENTITY_IDENTIFIERS.get());
+        entityPacket.setIdentifiers(Registries.BEDROCK_ENTITY_IDENTIFIERS.get());
         upstream.sendPacket(entityPacket);
 
         CreativeContentPacket creativePacket = new CreativeContentPacket();
@@ -999,7 +999,7 @@ public class GeyserSession implements CommandSender {
 
 
             for (Tickable entity : entityCache.getTickableEntities()) {
-                entity.tick(this);
+                entity.tick();
             }
         } catch (Throwable throwable) {
             throwable.printStackTrace();
@@ -1030,7 +1030,7 @@ public class GeyserSession implements CommandSender {
             collisionManager.updateScaffoldingFlags(false);
         }
 
-        playerEntity.updateBedrockMetadata(this);
+        playerEntity.updateBedrockMetadata();
 
         if (mouseoverEntity != null) {
             // Horses, etc can change their property depending on if you're sneaking
@@ -1040,17 +1040,17 @@ public class GeyserSession implements CommandSender {
 
     private void setSneakingPose(boolean sneaking) {
         this.pose = sneaking ? Pose.SNEAKING : Pose.STANDING;
-        playerEntity.getMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, sneaking ? 1.5f : playerEntity.getEntityType().getHeight());
-        playerEntity.getMetadata().getFlags().setFlag(EntityFlag.SNEAKING, sneaking);
+        playerEntity.getDirtyMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, sneaking ? 1.5f : playerEntity.getDefinition().height());
+        playerEntity.setFlag(EntityFlag.SNEAKING, sneaking);
 
         collisionManager.updatePlayerBoundingBox();
     }
 
     public void setSwimming(boolean swimming) {
         this.pose = swimming ? Pose.SWIMMING : Pose.STANDING;
-        playerEntity.getMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, swimming ? 0.6f : playerEntity.getEntityType().getHeight());
-        playerEntity.getMetadata().getFlags().setFlag(EntityFlag.SWIMMING, swimming);
-        playerEntity.updateBedrockMetadata(this);
+        playerEntity.getDirtyMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, swimming ? 0.6f : playerEntity.getDefinition().height());
+        playerEntity.setFlag(EntityFlag.SWIMMING, swimming);
+        playerEntity.updateBedrockMetadata();
     }
 
     public void setFlying(boolean flying) {
@@ -1059,7 +1059,7 @@ public class GeyserSession implements CommandSender {
         if (sneaking) {
             // update bounding box as it is not reduced when flying
             setSneakingPose(!flying);
-            playerEntity.updateBedrockMetadata(this);
+            playerEntity.updateBedrockMetadata();
         }
     }
 
@@ -1072,7 +1072,7 @@ public class GeyserSession implements CommandSender {
         AttributeData currentPlayerSpeed = playerEntity.getAttributes().get(GeyserAttributeType.MOVEMENT_SPEED);
         if (currentPlayerSpeed != null) {
             if ((pose.equals(Pose.SNEAKING) && !sneaking && collisionManager.isUnderSlab()) ||
-                    (!swimmingInWater && playerEntity.getMetadata().getFlags().getFlag(EntityFlag.SWIMMING) && !collisionManager.isPlayerInWater())) {
+                    (!swimmingInWater && playerEntity.getDirtyMetadata().getFlags().getFlag(EntityFlag.SWIMMING) && !collisionManager.isPlayerInWater())) {
                 // Either of those conditions means that Bedrock goes zoom when they shouldn't be
                 AttributeData speedAttribute = GeyserAttributeType.MOVEMENT_SPEED.getAttribute(originalSpeedAttribute / 3.32f);
                 playerEntity.getAttributes().put(GeyserAttributeType.MOVEMENT_SPEED, speedAttribute);
@@ -1282,7 +1282,7 @@ public class GeyserSession implements CommandSender {
             if (resendID != -1) {
                 connector.getLogger().debug("Resending teleport " + resendID);
                 TeleportCache teleport = teleportMap.get(resendID);
-                getPlayerEntity().moveAbsolute(this, Vector3f.from(teleport.getX(), teleport.getY(), teleport.getZ()),
+                getPlayerEntity().moveAbsolute(Vector3f.from(teleport.getX(), teleport.getY(), teleport.getZ()),
                         teleport.getYaw(), teleport.getPitch(), playerEntity.isOnGround(), true);
             }
         }
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java
index 3ff547c95..10b7159aa 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java
@@ -31,7 +31,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.objects.ObjectArrayList;
-import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
 import lombok.Getter;
 import org.geysermc.connector.entity.Entity;
 import org.geysermc.connector.entity.Tickable;
@@ -46,8 +45,6 @@ import java.util.concurrent.atomic.AtomicLong;
  * for that player (e.g. seeing vanished players from /vanish)
  */
 public class EntityCache {
-    private final GeyserSession session;
-
     @Getter
     private final Long2ObjectMap<Entity> entities = new Long2ObjectOpenHashMap<>();
     /**
@@ -63,13 +60,12 @@ public class EntityCache {
     private final AtomicLong nextEntityId = new AtomicLong(2L);
 
     public EntityCache(GeyserSession session) {
-        this.session = session;
         cachedPlayerEntityLinks.defaultReturnValue(-1L);
     }
 
     public void spawnEntity(Entity entity) {
         if (cacheEntity(entity)) {
-            entity.spawnEntity(session);
+            entity.spawnEntity();
 
             if (entity instanceof Tickable) {
                 // Start ticking it
@@ -89,7 +85,7 @@ public class EntityCache {
     }
 
     public boolean removeEntity(Entity entity, boolean force) {
-        if (entity != null && entity.isValid() && (force || entity.despawnEntity(session))) {
+        if (entity != null && entity.isValid() && (force || entity.despawnEntity())) {
             long geyserId = entityIdTranslations.remove(entity.getEntityId());
             entities.remove(geyserId);
 
@@ -102,9 +98,9 @@ public class EntityCache {
     }
 
     public void removeAllEntities() {
-        List<Entity> entities = new ArrayList<>(session.getEntityCache().getEntities().values());
+        List<Entity> entities = new ArrayList<>(this.entities.values());
         for (Entity entity : entities) {
-            session.getEntityCache().removeEntity(entity, false);
+            removeEntity(entity, false);
         }
 
         // As a precaution
@@ -119,16 +115,6 @@ public class EntityCache {
         return entities.get(entityIdTranslations.get(javaId));
     }
 
-    public <T extends Entity> Set<T> getEntitiesByType(Class<T> entityType) {
-        Set<T> entitiesOfType = new ObjectOpenHashSet<>();
-        for (Entity entity : (entityType == PlayerEntity.class ? playerEntities : entities).values()) {
-            if (entity.is(entityType)) {
-                entitiesOfType.add(entity.as(entityType));
-            }
-        }
-        return entitiesOfType;
-    }
-
     public void addPlayerEntity(PlayerEntity entity) {
         playerEntities.put(entity.getUuid(), entity);
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java
index a0bed9ad4..14efca357 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java
@@ -121,7 +121,7 @@ public class PistonCache {
             SessionPlayerEntity playerEntity = session.getPlayerEntity();
             boolean isOnGround = playerDisplacement.getY() > 0 || playerEntity.isOnGround();
             Vector3d position = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter();
-            playerEntity.moveAbsolute(session, position.toFloat(), playerEntity.getRotation(), isOnGround, true);
+            playerEntity.moveAbsolute(position.toFloat(), playerEntity.getYaw(), playerEntity.getPitch(), playerEntity.getHeadYaw(), isOnGround, true);
         }
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldBorder.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldBorder.java
index f312bb8b4..9d51f82d2 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldBorder.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldBorder.java
@@ -33,8 +33,8 @@ import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
 import com.nukkitx.protocol.bedrock.packet.PlayerFogPacket;
 import lombok.Getter;
 import lombok.Setter;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.player.PlayerEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 
 import javax.annotation.Nonnull;
@@ -150,9 +150,8 @@ public class WorldBorder {
             PlayerEntity playerEntity = session.getPlayerEntity();
             // Move the player back, but allow gravity to take place
             // Teleported = true makes going back better, but disconnects the player from their mounted entity
-            playerEntity.moveAbsolute(session,
-                    Vector3f.from(playerEntity.getPosition().getX(), (newPosition.getY() - EntityType.PLAYER.getOffset()), playerEntity.getPosition().getZ()),
-                    playerEntity.getRotation(), playerEntity.isOnGround(), session.getRidingVehicleEntity() == null);
+            playerEntity.moveAbsolute(Vector3f.from(playerEntity.getPosition().getX(), (newPosition.getY() - EntityDefinitions.PLAYER.offset()), playerEntity.getPosition().getZ()),
+                    playerEntity.getYaw(), playerEntity.getPitch(), playerEntity.getHeadYaw(), playerEntity.isOnGround(), session.getRidingVehicleEntity() == null);
         }
         return isInWorldBorder;
     }
@@ -261,7 +260,7 @@ public class WorldBorder {
     }
 
     private void drawWall(Vector3f position, boolean drawWallX) {
-        int initialY = (int) (position.getY() - EntityType.PLAYER.getOffset() - 1);
+        int initialY = (int) (position.getY() - EntityDefinitions.PLAYER.offset() - 1);
         for (int y = initialY; y < (initialY + 5); y++) {
             if (drawWallX) {
                 float x = position.getX();
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockAdventureSettingsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockAdventureSettingsTranslator.java
index 8a978fac3..10138f9a3 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockAdventureSettingsTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockAdventureSettingsTranslator.java
@@ -50,7 +50,7 @@ public class BedrockAdventureSettingsTranslator extends PacketTranslator<Adventu
         ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(isFlying);
         session.sendDownstreamPacket(abilitiesPacket);
 
-        if (isFlying && session.getPlayerEntity().getMetadata().getFlags().getFlag(EntityFlag.SWIMMING)) {
+        if (isFlying && session.getPlayerEntity().getDirtyMetadata().getFlags().getFlag(EntityFlag.SWIMMING)) {
             // Bedrock can fly and swim at the same time? Make sure that can't happen
             session.setSwimming(false);
         }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestTranslator.java
index add16e9ca..24af60e6c 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestTranslator.java
@@ -27,8 +27,8 @@ package org.geysermc.connector.network.translators.bedrock;
 
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.packet.BlockPickRequestPacket;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.ItemFrameEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -55,7 +55,7 @@ public class BedrockBlockPickRequestTranslator extends PacketTranslator<BlockPic
                     InventoryUtils.findOrCreateItem(session, entity.getHeldItem());
                 } else {
                     // Grab the frame as the item
-                    InventoryUtils.findOrCreateItem(session, entity.getEntityType() == EntityType.GLOW_ITEM_FRAME ? "minecraft:glow_item_frame" : "minecraft:item_frame");
+                    InventoryUtils.findOrCreateItem(session, entity.getDefinition() == EntityDefinitions.GLOW_ITEM_FRAME ? "minecraft:glow_item_frame" : "minecraft:item_frame");
                 }
             }
             return;
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityPickRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityPickRequestTranslator.java
index c1febb795..669f2751d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityPickRequestTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityPickRequestTranslator.java
@@ -26,8 +26,8 @@
 package org.geysermc.connector.network.translators.bedrock;
 
 import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
-import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.packet.EntityPickRequestPacket;
+import org.geysermc.connector.entity.BoatEntity;
 import org.geysermc.connector.entity.Entity;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
@@ -35,6 +35,8 @@ import org.geysermc.connector.network.translators.Translator;
 import org.geysermc.connector.registry.type.ItemMapping;
 import org.geysermc.connector.utils.InventoryUtils;
 
+import java.util.Locale;
+
 /**
  * Called when the Bedrock user uses the pick block button on an entity
  */
@@ -49,10 +51,10 @@ public class BedrockEntityPickRequestTranslator extends PacketTranslator<EntityP
 
         // Get the corresponding item
         String itemName;
-        switch (entity.getEntityType()) {
+        switch (entity.getDefinition().entityType()) {
             case BOAT -> {
                 // Include type of boat in the name
-                int variant = entity.getMetadata().getInt(EntityData.VARIANT);
+                int variant = ((BoatEntity) entity).getVariant();
                 String typeOfBoat = switch (variant) {
                     case 1 -> "spruce";
                     case 2 -> "birch";
@@ -65,15 +67,16 @@ public class BedrockEntityPickRequestTranslator extends PacketTranslator<EntityP
             }
             case LEASH_KNOT -> itemName = "lead";
             case MINECART_CHEST, MINECART_COMMAND_BLOCK, MINECART_FURNACE, MINECART_HOPPER, MINECART_TNT ->
-                    // Move MINECART to the end of the name
-                    itemName = entity.getEntityType().toString().toLowerCase().replace("minecart_", "") + "_minecart";
+                    // The Bedrock identifier matches the item name which moves MINECART to the end of the name
+                    // TODO test
+                    itemName = entity.getDefinition().identifier();
             case MINECART_SPAWNER -> itemName = "minecart"; // Turns into a normal minecart
             //case ITEM_FRAME -> Not an entity in Bedrock Edition
             //case GLOW_ITEM_FRAME ->
             case ARMOR_STAND, END_CRYSTAL, MINECART, PAINTING ->
                     // No spawn egg, just an item
-                    itemName = entity.getEntityType().toString().toLowerCase();
-            default -> itemName = entity.getEntityType().toString().toLowerCase() + "_spawn_egg";
+                    itemName = entity.getDefinition().entityType().toString().toLowerCase(Locale.ROOT);
+            default -> itemName = entity.getDefinition().entityType().toString().toLowerCase(Locale.ROOT) + "_spawn_egg";
         }
 
         String fullItemName = "minecraft:" + itemName;
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
index 06d9a69ee..baa679abc 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
@@ -26,11 +26,11 @@
 package org.geysermc.connector.network.translators.bedrock;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
+import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
 import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
 import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockFace;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
@@ -42,8 +42,8 @@ import com.nukkitx.protocol.bedrock.data.inventory.*;
 import com.nukkitx.protocol.bedrock.packet.*;
 import org.geysermc.connector.entity.CommandBlockMinecartEntity;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.ItemFrameEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.inventory.GeyserItemStack;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
@@ -92,7 +92,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         ServerboundPlayerActionPacket dropAllPacket = new ServerboundPlayerActionPacket(
                                 dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
                                 BlockUtils.POSITION_ZERO,
-                                BlockFace.DOWN
+                                Direction.DOWN
                         );
                         session.sendDownstreamPacket(dropAllPacket);
 
@@ -156,13 +156,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         // Adjust position for current eye height
                         switch (session.getPose()) {
                             case SNEAKING ->
-                                playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 1.27f), 0);
+                                playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 1.27f), 0);
                             case SWIMMING,
                                 FALL_FLYING, // Elytra
                                 SPIN_ATTACK -> // Trident spin attack
-                                playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.4f), 0);
+                                playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.4f), 0);
                             case SLEEPING ->
-                                playerPosition = playerPosition.sub(0, (EntityType.PLAYER.getOffset() - 0.2f), 0);
+                                playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.2f), 0);
                         } // else, we don't have to modify the position
 
                         float diffX = playerPosition.getX() - packet.getBlockPosition().getX();
@@ -175,7 +175,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         }
 
                         // Vanilla check
-                        if (!(session.getPlayerEntity().getPosition().sub(0, EntityType.PLAYER.getOffset(), 0)
+                        if (!(session.getPlayerEntity().getPosition().sub(0, EntityDefinitions.PLAYER.offset(), 0)
                                 .distanceSquared(packet.getBlockPosition().toFloat().add(0.5f, 0.5f, 0.5f)) < MAXIMUM_BLOCK_PLACING_DISTANCE)) {
                             // The client thinks that its blocks have been successfully placed. Restore the server's blocks instead.
                             restoreCorrectBlock(session, blockPos, packet);
@@ -198,7 +198,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
 
                         ServerboundUseItemOnPacket blockPacket = new ServerboundUseItemOnPacket(
                                 new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()),
-                                BlockFace.values()[packet.getBlockFace()],
+                                Direction.VALUES[packet.getBlockFace()],
                                 Hand.MAIN_HAND,
                                 packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(),
                                 false);
@@ -300,7 +300,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         Vector3f playerPosition = session.getPlayerEntity().getPosition();
                         Vector3f floatBlockPosition = packet.getBlockPosition().toFloat();
                         float diffX = playerPosition.getX() - (floatBlockPosition.getX() + 0.5f);
-                        float diffY = (playerPosition.getY() - EntityType.PLAYER.getOffset()) - (floatBlockPosition.getY() + 0.5f) + 1.5f;
+                        float diffY = (playerPosition.getY() - EntityDefinitions.PLAYER.offset()) - (floatBlockPosition.getY() + 0.5f) + 1.5f;
                         float diffZ = playerPosition.getZ() - (floatBlockPosition.getZ() + 0.5f);
                         float distanceSquared = diffX * diffX + diffY * diffY + diffZ * diffZ;
                         if (distanceSquared > MAXIMUM_BLOCK_DESTROYING_DISTANCE) {
@@ -325,7 +325,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
 
                         PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
                         Position pos = new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ());
-                        ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, pos, BlockFace.values()[packet.getBlockFace()]);
+                        ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, pos, Direction.VALUES[packet.getBlockFace()]);
                         session.sendDownstreamPacket(breakPacket);
                     }
                 }
@@ -334,7 +334,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                 if (packet.getActionType() == 0) {
                     // Followed to the Minecraft Protocol specification outlined at wiki.vg
                     ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO,
-                            BlockFace.DOWN);
+                            Direction.DOWN);
                     session.sendDownstreamPacket(releaseItemPacket);
                 }
                 break;
@@ -369,7 +369,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         EntitySoundInteractionHandler.handleEntityInteraction(session, packet.getClickPosition(), entity);
                         break;
                     case 1: //Attack
-                        if (entity.getEntityType() == EntityType.ENDER_DRAGON) {
+                        if (entity.getDefinition() == EntityDefinitions.ENDER_DRAGON) {
                             // Redirects the attack to its body entity, this only happens when
                             // attacking the underbelly of the ender dragon
                             ServerboundInteractPacket attackPacket = new ServerboundInteractPacket((int) entity.getEntityId() + 3,
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java
index 5c905dfe6..e64b16a92 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java
@@ -26,11 +26,11 @@
 package org.geysermc.connector.network.translators.bedrock;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
+import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockFace;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerButtonClickPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClosePacket;
+import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket;
 import com.nukkitx.protocol.bedrock.packet.LecternUpdatePacket;
 import org.geysermc.connector.inventory.LecternContainer;
 import org.geysermc.connector.network.session.GeyserSession;
@@ -54,7 +54,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
             // Emulate an interact packet
             ServerboundUseItemOnPacket blockPacket = new ServerboundUseItemOnPacket(
                     new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()),
-                    BlockFace.DOWN,
+                    Direction.DOWN,
                     Hand.MAIN_HAND,
                     0, 0, 0, // Java doesn't care about these when dealing with a lectern
                     false);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMoveEntityAbsoluteTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMoveEntityAbsoluteTranslator.java
index b1e894d0a..e2ece91c8 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMoveEntityAbsoluteTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMoveEntityAbsoluteTranslator.java
@@ -29,7 +29,8 @@ import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.Serverb
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
 import org.geysermc.connector.entity.BoatEntity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -44,27 +45,28 @@ public class BedrockMoveEntityAbsoluteTranslator extends PacketTranslator<MoveEn
     public void translate(GeyserSession session, MoveEntityAbsolutePacket packet) {
         session.setLastVehicleMoveTimestamp(System.currentTimeMillis());
 
-        if (session.getRidingVehicleEntity() != null && session.getWorldBorder().isPassingIntoBorderBoundaries(packet.getPosition(), false)) {
-            Vector3f position = Vector3f.from(session.getRidingVehicleEntity().getPosition().getX(), packet.getPosition().getY(),
-                    session.getRidingVehicleEntity().getPosition().getZ());
-            if (session.getRidingVehicleEntity() instanceof BoatEntity) {
+        Entity ridingEntity = session.getRidingVehicleEntity();
+        if (ridingEntity != null && session.getWorldBorder().isPassingIntoBorderBoundaries(packet.getPosition(), false)) {
+            Vector3f position = Vector3f.from(ridingEntity.getPosition().getX(), packet.getPosition().getY(),
+                    ridingEntity.getPosition().getZ());
+            if (ridingEntity instanceof BoatEntity) {
                 // Undo the changes usually applied to the boat
-                session.getRidingVehicleEntity().as(BoatEntity.class).moveAbsoluteWithoutAdjustments(session,
-                        position, session.getRidingVehicleEntity().getRotation(),
-                        session.getRidingVehicleEntity().isOnGround(), true);
+                ridingEntity.as(BoatEntity.class)
+                        .moveAbsoluteWithoutAdjustments(position, ridingEntity.getYaw(),
+                        ridingEntity.isOnGround(), true);
             } else {
                 // This doesn't work if teleported is false
-                session.getRidingVehicleEntity().moveAbsolute(session, position,
-                        session.getRidingVehicleEntity().getRotation(),
-                        session.getRidingVehicleEntity().isOnGround(), true);
+                ridingEntity.moveAbsolute(position,
+                        ridingEntity.getYaw(), ridingEntity.getPitch(), ridingEntity.getHeadYaw(),
+                        ridingEntity.isOnGround(), true);
             }
             return;
         }
 
         float y = packet.getPosition().getY();
-        if (session.getRidingVehicleEntity() instanceof BoatEntity) {
+        if (ridingEntity instanceof BoatEntity) {
             // Remove the offset to prevents boats from looking like they're floating in water
-            y -= EntityType.BOAT.getOffset();
+            y -= EntityDefinitions.BOAT.offset();
         }
         ServerboundMoveVehiclePacket ServerboundMoveVehiclePacket = new ServerboundMoveVehiclePacket(
                 packet.getPosition().getX(), y, packet.getPosition().getZ(),
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockPlayerInputTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockPlayerInputTranslator.java
index 8cc7568b5..c19936b00 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockPlayerInputTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockPlayerInputTranslator.java
@@ -25,16 +25,16 @@
 
 package org.geysermc.connector.network.translators.bedrock;
 
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.ServerboundPlayerInputPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.ServerboundMoveVehiclePacket;
+import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.ServerboundPlayerInputPacket;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.packet.PlayerInputPacket;
 import org.geysermc.connector.entity.BoatEntity;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.living.animal.horse.AbstractHorseEntity;
 import org.geysermc.connector.entity.living.animal.horse.LlamaEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -65,7 +65,7 @@ public class BedrockPlayerInputTranslator extends PacketTranslator<PlayerInputPa
                 sendMovement = true;
             } else {
                 // Check if the player is the front rider
-                Vector3f seatPos = session.getPlayerEntity().getMetadata().getVector3f(EntityData.RIDER_SEAT_POSITION, null);
+                Vector3f seatPos = session.getPlayerEntity().getDirtyMetadata().getVector3f(EntityData.RIDER_SEAT_POSITION, null);
                 if (seatPos != null && seatPos.getX() > 0) {
                     sendMovement = true;
                 }
@@ -75,16 +75,15 @@ public class BedrockPlayerInputTranslator extends PacketTranslator<PlayerInputPa
             long timeSinceVehicleMove = System.currentTimeMillis() - session.getLastVehicleMoveTimestamp();
             if (timeSinceVehicleMove >= 100) {
                 Vector3f vehiclePosition = vehicle.getPosition();
-                Vector3f vehicleRotation = vehicle.getRotation();
 
                 if (vehicle instanceof BoatEntity) {
                     // Remove some Y position to prevents boats flying up
-                    vehiclePosition = vehiclePosition.down(EntityType.BOAT.getOffset());
+                    vehiclePosition = vehiclePosition.down(EntityDefinitions.BOAT.offset());
                 }
 
                 ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(
                         vehiclePosition.getX(), vehiclePosition.getY(), vehiclePosition.getZ(),
-                        vehicleRotation.getX() - 90, vehicleRotation.getY()
+                        vehicle.getYaw() - 90, vehicle.getPitch()
                 );
                 session.sendDownstreamPacket(moveVehiclePacket);
             }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockRespawnTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockRespawnTranslator.java
index 8a70ada6f..8d1fda4e8 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockRespawnTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockRespawnTranslator.java
@@ -57,7 +57,7 @@ public class BedrockRespawnTranslator extends PacketTranslator<RespawnPacket> {
                 if (entity == null) return;
                 SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
                 entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
-                entityDataPacket.getMetadata().putAll(entity.getMetadata());
+                entityDataPacket.getMetadata().putAll(entity.getDirtyMetadata());
                 session.sendUpstreamPacket(entityDataPacket);
 
                 MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java
index 9b25a6486..f46f486e9 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java
@@ -62,8 +62,8 @@ public class BedrockEntityEventTranslator extends PacketTranslator<EntityEventPa
                         if (trades != null && packet.getData() >= 0 && packet.getData() < trades.length) {
                             VillagerTrade trade = merchantInventory.getVillagerTrades()[packet.getData()];
                             openInventory.setItem(2, GeyserItemStack.from(trade.getOutput()), session);
-                            villager.getMetadata().put(EntityData.TRADE_XP, trade.getXp() + villager.getMetadata().getInt(EntityData.TRADE_XP));
-                            villager.updateBedrockMetadata(session);
+                            villager.getDirtyMetadata().put(EntityData.TRADE_XP, trade.getXp() + villager.getDirtyMetadata().getInt(EntityData.TRADE_XP));
+                            villager.updateBedrockMetadata();
                         }
                     }
                 }, 100, TimeUnit.MILLISECONDS);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java
index 497de45f8..cbc01f8ee 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java
@@ -26,8 +26,8 @@
 package org.geysermc.connector.network.translators.bedrock.entity.player;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
+import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.*;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockFace;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
@@ -116,7 +116,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                         useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND);
                     }
                     session.sendDownstreamPacket(useItemPacket);
-                    session.getPlayerEntity().getMetadata().getFlags().setFlag(EntityFlag.BLOCKING, true);
+                    session.getPlayerEntity().getDirtyMetadata().getFlags().setFlag(EntityFlag.BLOCKING, true);
                     // metadata will be updated when sneaking
                 }
 
@@ -127,10 +127,10 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 session.sendDownstreamPacket(stopSneakPacket);
 
                 // Stop shield, if necessary
-                if (session.getPlayerEntity().getMetadata().getFlags().getFlag(EntityFlag.BLOCKING)) {
-                    ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO, BlockFace.DOWN);
+                if (session.getPlayerEntity().getDirtyMetadata().getFlags().getFlag(EntityFlag.BLOCKING)) {
+                    ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO, Direction.DOWN);
                     session.sendDownstreamPacket(releaseItemPacket);
-                    session.getPlayerEntity().getMetadata().getFlags().setFlag(EntityFlag.BLOCKING, false);
+                    session.getPlayerEntity().getDirtyMetadata().getFlags().setFlag(EntityFlag.BLOCKING, false);
                     // metadata will be updated when sneaking
                 }
 
@@ -147,7 +147,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 session.setSprinting(false);
                 break;
             case DROP_ITEM:
-                ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, position, BlockFace.values()[packet.getFace()]);
+                ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, position, Direction.VALUES[packet.getFace()]);
                 session.sendDownstreamPacket(dropItemPacket);
                 break;
             case STOP_SLEEP:
@@ -181,14 +181,14 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 String identifier = BlockRegistries.JAVA_IDENTIFIERS.get().get(blockUp);
                 if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) {
                     ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, new Position(fireBlockPos.getX(),
-                            fireBlockPos.getY(), fireBlockPos.getZ()), BlockFace.values()[packet.getFace()]);
+                            fireBlockPos.getY(), fireBlockPos.getZ()), Direction.VALUES[packet.getFace()]);
                     session.sendDownstreamPacket(startBreakingPacket);
                     if (session.getGameMode() == GameMode.CREATIVE) {
                         break;
                     }
                 }
 
-                ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, position, BlockFace.values()[packet.getFace()]);
+                ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, position, Direction.VALUES[packet.getFace()]);
                 session.sendDownstreamPacket(startBreakingPacket);
                 break;
             case CONTINUE_BREAK:
@@ -223,7 +223,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                     }
                 }
 
-                ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, position, BlockFace.DOWN);
+                ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, position, Direction.DOWN);
                 session.sendDownstreamPacket(abortBreakingPacket);
                 LevelEventPacket stopBreak = new LevelEventPacket();
                 stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockEmoteTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockEmoteTranslator.java
index 60cbe6b08..1e5f1eb3c 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockEmoteTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockEmoteTranslator.java
@@ -25,8 +25,8 @@
 
 package org.geysermc.connector.network.translators.bedrock.entity.player;
 
+import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockFace;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
 import com.nukkitx.protocol.bedrock.packet.EmotePacket;
 import org.geysermc.connector.configuration.EmoteOffhandWorkaroundOption;
@@ -44,7 +44,7 @@ public class BedrockEmoteTranslator extends PacketTranslator<EmotePacket> {
         if (session.getConnector().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) {
             // Activate the workaround - we should trigger the offhand now
             ServerboundPlayerActionPacket swapHandsPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, BlockUtils.POSITION_ZERO,
-                    BlockFace.DOWN);
+                    Direction.DOWN);
             session.sendDownstreamPacket(swapHandsPacket);
 
             if (session.getConnector().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java
index 9a307d076..fbbc9ffc6 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java
@@ -90,8 +90,8 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
                     if (session.getMouseoverEntity() != null) {
                         // No interactive tag should be sent
                         session.setMouseoverEntity(null);
-                        session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, "");
-                        session.getPlayerEntity().updateBedrockMetadata(session);
+                        session.getPlayerEntity().getDirtyMetadata().put(EntityData.INTERACTIVE_TAG, "");
+                        session.getPlayerEntity().updateBedrockMetadata();
                     }
                 }
                 break;
@@ -99,7 +99,7 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
                 if (session.getOpenInventory() == null) {
                     Entity ridingEntity = session.getRidingVehicleEntity();
                     if (ridingEntity instanceof AbstractHorseEntity) {
-                        if (ridingEntity.getMetadata().getFlags().getFlag(EntityFlag.TAMED)) {
+                        if (ridingEntity.getDirtyMetadata().getFlags().getFlag(EntityFlag.TAMED)) {
                             // We should request to open the horse inventory instead
                             ServerboundPlayerCommandPacket openHorseWindowPacket = new ServerboundPlayerCommandPacket((int) session.getPlayerEntity().getEntityId(), PlayerState.OPEN_HORSE_INVENTORY);
                             session.sendDownstreamPacket(openHorseWindowPacket);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java
index 85d3a945e..609ee6f2d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java
@@ -34,8 +34,8 @@ import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
 import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
 import org.geysermc.connector.common.ChatColor;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.player.SessionPlayerEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -68,14 +68,17 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
         session.getBookEditCache().checkForSend();
 
         if (!session.getTeleportMap().isEmpty()) {
-            session.confirmTeleport(packet.getPosition().toDouble().sub(0, EntityType.PLAYER.getOffset(), 0));
+            session.confirmTeleport(packet.getPosition().toDouble().sub(0, EntityDefinitions.PLAYER.offset(), 0));
             return;
         }
         // head yaw, pitch, head yaw
         Vector3f rotation = Vector3f.from(packet.getRotation().getY(), packet.getRotation().getX(), packet.getRotation().getY());
+        float yaw = packet.getRotation().getY();
+        float pitch = packet.getRotation().getX();
+        float headYaw = packet.getRotation().getY();
 
         boolean positionChanged = !entity.getPosition().equals(packet.getPosition());
-        boolean rotationChanged = !entity.getRotation().equals(rotation);
+        boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw;
 
         // If only the pitch and yaw changed
         // This isn't needed, but it makes the packets closer to vanilla
@@ -84,7 +87,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
             ServerboundMovePlayerRotPacket playerRotationPacket = new ServerboundMovePlayerRotPacket(
                     packet.isOnGround(), packet.getRotation().getY(), packet.getRotation().getX());
 
-            entity.setRotation(rotation);
+            entity.setYaw(yaw);
+            entity.setPitch(pitch);
+            entity.setHeadYaw(headYaw);
             entity.setOnGround(packet.isOnGround());
 
             session.sendDownstreamPacket(playerRotationPacket);
@@ -101,7 +106,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
                         // Send rotation updates as well
                         movePacket = new ServerboundMovePlayerPosRotPacket(packet.isOnGround(), position.getX(), position.getY(), position.getZ(),
                                 packet.getRotation().getY(), packet.getRotation().getX());
-                        entity.setRotation(rotation);
+                        entity.setYaw(yaw);
+                        entity.setPitch(pitch);
+                        entity.setHeadYaw(headYaw);
                     } else {
                         // Rotation did not change; don't send an update with rotation
                         movePacket = new ServerboundMovePlayerPosPacket(packet.isOnGround(), position.getX(), position.getY(), position.getZ());
@@ -145,10 +152,10 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
 
         // Move parrots to match if applicable
         if (entity.getLeftParrot() != null) {
-            entity.getLeftParrot().moveAbsolute(session, entity.getPosition(), entity.getRotation(), true, false);
+            entity.getLeftParrot().moveAbsolute(entity.getPosition(), entity.getYaw(), entity.getPitch(), entity.getHeadYaw(), true, false);
         }
         if (entity.getRightParrot() != null) {
-            entity.getRightParrot().moveAbsolute(session, entity.getPosition(), entity.getRotation(), true, false);
+            entity.getRightParrot().moveAbsolute(entity.getPosition(), entity.getYaw(), entity.getPitch(), entity.getHeadYaw(), true, false);
         }
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java
index bb9303447..2f5c41ae9 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java
@@ -30,21 +30,21 @@ import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
-import com.nukkitx.protocol.bedrock.data.entity.EntityFlags;
 import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
 import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
 import lombok.Getter;
 import lombok.Setter;
+import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.player.PlayerEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.session.cache.PistonCache;
 import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
 import org.geysermc.connector.network.translators.collision.translators.ScaffoldingCollision;
 import org.geysermc.connector.network.translators.world.block.BlockStateValues;
+import org.geysermc.connector.utils.Axis;
 import org.geysermc.connector.utils.BlockPositionIterator;
 import org.geysermc.connector.utils.BlockUtils;
-import org.geysermc.connector.utils.Axis;
 
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
@@ -121,7 +121,7 @@ public class CollisionManager {
         // - In Bedrock Edition, the height becomes 1.65 blocks, allowing movement through spaces as small as 1.75 (2 - 1⁄4) blocks high.
         // - In Java Edition, the height becomes 1.5 blocks.
         // Other instances have the player's bounding box become as small as 0.6 or 0.2.
-        double playerHeight = session.getPlayerEntity().getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT);
+        double playerHeight = session.getPlayerEntity().getDirtyMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT);
         playerBoundingBox.setMiddleY(playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2.0) + (playerHeight / 2.0));
         playerBoundingBox.setSizeY(playerHeight);
     }
@@ -143,7 +143,7 @@ public class CollisionManager {
         }
         // We need to parse the float as a string since casting a float to a double causes us to
         // lose precision and thus, causes players to get stuck when walking near walls
-        double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset();
+        double javaY = bedrockPosition.getY() - EntityDefinitions.PLAYER.offset();
 
         Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY,
                 Double.parseDouble(Float.toString(bedrockPosition.getZ())));
@@ -172,7 +172,7 @@ public class CollisionManager {
         if (onGround != newOnGround || movement.distanceSquared(adjustedMovement) > INCORRECT_MOVEMENT_THRESHOLD) {
             PlayerEntity playerEntity = session.getPlayerEntity();
             if (pistonCache.getPlayerMotion().equals(Vector3f.ZERO) && !pistonCache.isPlayerSlimeCollision()) {
-                playerEntity.moveAbsolute(session, position.toFloat(), playerEntity.getRotation(), newOnGround, true);
+                playerEntity.moveAbsolute(position.toFloat(), playerEntity.getYaw(), playerEntity.getPitch(), playerEntity.getHeadYaw(), newOnGround, true);
             }
         }
 
@@ -190,7 +190,7 @@ public class CollisionManager {
         // Gravity might need to be reset...
         SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
         entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
-        entityDataPacket.getMetadata().putAll(entity.getMetadata());
+        entityDataPacket.getMetadata().putAll(entity.getDirtyMetadata());
         session.sendUpstreamPacket(entityDataPacket);
 
         MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
@@ -376,9 +376,9 @@ public class CollisionManager {
             // at the current location.
             double originalY = playerBoundingBox.getMiddleY();
             double originalHeight = playerBoundingBox.getSizeY();
-            double standingY = originalY - (originalHeight / 2.0) + (EntityType.PLAYER.getHeight() / 2.0);
+            double standingY = originalY - (originalHeight / 2.0) + (EntityDefinitions.PLAYER.height() / 2.0);
 
-            playerBoundingBox.setSizeY(EntityType.PLAYER.getHeight());
+            playerBoundingBox.setSizeY(EntityDefinitions.PLAYER.height());
             playerBoundingBox.setMiddleY(standingY);
             boolean result = collision.checkIntersection(position, playerBoundingBox);
             result |= session.getPistonCache().checkCollision(position, playerBoundingBox);
@@ -403,18 +403,17 @@ public class CollisionManager {
      * @param updateMetadata whether we should update metadata if something changed
      */
     public void updateScaffoldingFlags(boolean updateMetadata) {
-        EntityFlags flags = session.getPlayerEntity().getMetadata().getFlags();
-        boolean flagsChanged;
+        Entity entity = session.getPlayerEntity();
         boolean isSneakingWithScaffolding = (touchingScaffolding || onScaffolding) && session.isSneaking();
 
-        flagsChanged = flags.setFlag(EntityFlag.OVER_DESCENDABLE_BLOCK, onScaffolding);
-        flagsChanged |= flags.setFlag(EntityFlag.IN_ASCENDABLE_BLOCK, touchingScaffolding);
-        flagsChanged |= flags.setFlag(EntityFlag.OVER_SCAFFOLDING, isSneakingWithScaffolding);
+        entity.setFlag(EntityFlag.OVER_DESCENDABLE_BLOCK, onScaffolding);
+        entity.setFlag(EntityFlag.IN_ASCENDABLE_BLOCK, touchingScaffolding);
+        entity.setFlag(EntityFlag.OVER_SCAFFOLDING, isSneakingWithScaffolding);
 
-        flagsChanged |= flags.setFlag(EntityFlag.IN_SCAFFOLDING, touchingScaffolding);
+        entity.setFlag(EntityFlag.IN_SCAFFOLDING, touchingScaffolding);
 
-        if (flagsChanged && updateMetadata) {
-            session.getPlayerEntity().updateBedrockMetadata(session);
+        if (updateMetadata) {
+            session.getPlayerEntity().updateBedrockMetadata();
         }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/MerchantInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/MerchantInventoryTranslator.java
index 5f20a9d03..be3b09b50 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/MerchantInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/MerchantInventoryTranslator.java
@@ -28,7 +28,6 @@ package org.geysermc.connector.network.translators.inventory.translators;
 import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;
 import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData;
 import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
 import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest;
@@ -36,7 +35,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
 import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
 import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket;
 import org.geysermc.connector.entity.Entity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.inventory.MerchantContainer;
 import org.geysermc.connector.inventory.PlayerInventory;
@@ -99,14 +98,15 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator {
             long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet();
             Vector3f pos = session.getPlayerEntity().getPosition().sub(0, 3, 0);
 
-            EntityDataMap metadata = new EntityDataMap();
-            metadata.put(EntityData.SCALE, 0f);
-            metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0f);
-            metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0f);
-
-            Entity villager = new Entity(0, geyserId, EntityType.VILLAGER, pos, Vector3f.ZERO, Vector3f.ZERO);
-            villager.setMetadata(metadata);
-            villager.spawnEntity(session);
+            Entity villager = new Entity(session, 0, geyserId, null, EntityDefinitions.VILLAGER, pos, Vector3f.ZERO, 0f, 0f, 0f) {
+                @Override
+                protected void initializeMetadata() {
+                    dirtyMetadata.put(EntityData.SCALE, 0f);
+                    dirtyMetadata.put(EntityData.BOUNDING_BOX_WIDTH, 0f);
+                    dirtyMetadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0f);
+                }
+            };
+            villager.spawnEntity();
 
             SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
             EntityLinkData.Type type = EntityLinkData.Type.PASSENGER;
@@ -127,7 +127,7 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator {
     public void closeInventory(GeyserSession session, Inventory inventory) {
         MerchantContainer merchantInventory = (MerchantContainer) inventory;
         if (merchantInventory.getVillager() != null) {
-            merchantInventory.getVillager().despawnEntity(session);
+            merchantInventory.getVillager().despawnEntity();
         }
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaCommandsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaCommandsTranslator.java
index ddc05b62f..8eca69333 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaCommandsTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaCommandsTranslator.java
@@ -43,12 +43,12 @@ import lombok.Getter;
 import lombok.ToString;
 import net.kyori.adventure.text.format.NamedTextColor;
 import org.geysermc.connector.GeyserConnector;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
 import org.geysermc.connector.network.translators.item.Enchantment;
 import org.geysermc.connector.registry.BlockRegistries;
+import org.geysermc.connector.registry.Registries;
 import org.geysermc.connector.utils.EntityUtils;
 
 import java.util.*;
@@ -220,7 +220,7 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
             case BLOCK_STATE -> BlockRegistries.JAVA_TO_BEDROCK_IDENTIFIERS.get().keySet().toArray(new String[0]);
             case ITEM_STACK -> session.getItemMappings().getItemNames();
             case ITEM_ENCHANTMENT -> Enchantment.JavaEnchantment.ALL_JAVA_IDENTIFIERS;
-            case ENTITY_SUMMON -> EntityType.ALL_JAVA_IDENTIFIERS;
+            case ENTITY_SUMMON -> Registries.JAVA_ENTITY_IDENTIFIERS.get().keySet().toArray(new String[0]); //TODO add Marker
             case COLOR -> VALID_COLORS;
             case SCOREBOARD_SLOT -> VALID_SCOREBOARD_SLOTS;
             case MOB_EFFECT -> ALL_EFFECT_IDENTIFIERS;
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java
index cbcbe9f1d..ca7c92302 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java
@@ -92,7 +92,7 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
 
         SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
         entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
-        entityDataPacket.getMetadata().putAll(entity.getMetadata());
+        entityDataPacket.getMetadata().putAll(entity.getDirtyMetadata());
         session.sendUpstreamPacket(entityDataPacket);
 
         // Send if client should show respawn screen
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityEventTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityEventTranslator.java
index 9774b2456..82cbe7803 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityEventTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityEventTranslator.java
@@ -33,8 +33,8 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
 import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
 import com.nukkitx.protocol.bedrock.packet.*;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.LivingEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -96,7 +96,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
                 break;
             case LIVING_DEATH:
                 entityEventPacket.setType(EntityEventType.DEATH);
-                if (entity.getEntityType() == EntityType.THROWN_EGG) {
+                if (entity.getDefinition() == EntityDefinitions.THROWN_EGG) {
                     LevelEventPacket particlePacket = new LevelEventPacket();
                     particlePacket.setType(LevelEventType.PARTICLE_ITEM_BREAK);
                     particlePacket.setData(session.getItemMappings().getStoredItems().egg().getBedrockId() << 16);
@@ -104,7 +104,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
                     for (int i = 0; i < 6; i++) {
                         session.sendUpstreamPacket(particlePacket);
                     }
-                } else if (entity.getEntityType() == EntityType.SNOWBALL) {
+                } else if (entity.getDefinition() == EntityDefinitions.SNOWBALL) {
                     LevelEventPacket particlePacket = new LevelEventPacket();
                     particlePacket.setType(LevelEventType.PARTICLE_SNOWBALL_POOF);
                     particlePacket.setPosition(entity.getPosition());
@@ -122,9 +122,9 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
             case FISHING_HOOK_PULL_PLAYER:
                 // Player is pulled from a fishing rod
                 // The physics of this are clientside on Java
-                long pulledById = entity.getMetadata().getLong(EntityData.TARGET_EID);
+                long pulledById = entity.getDirtyMetadata().getLong(EntityData.TARGET_EID);
                 if (session.getPlayerEntity().getGeyserId() == pulledById) {
-                    Entity hookOwner = session.getEntityCache().getEntityByGeyserId(entity.getMetadata().getLong(EntityData.OWNER_EID));
+                    Entity hookOwner = session.getEntityCache().getEntityByGeyserId(entity.getDirtyMetadata().getLong(EntityData.OWNER_EID));
                     if (hookOwner != null) {
                         // https://minecraft.gamepedia.com/Fishing_Rod#Hooking_mobs_and_other_entities
                         SetEntityMotionPacket motionPacket = new SetEntityMotionPacket();
@@ -169,7 +169,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
                 session.sendUpstreamPacket(playSoundPacket);
                 break;
             case SHEEP_GRAZE_OR_TNT_CART_EXPLODE:
-                if (entity.getEntityType() == EntityType.SHEEP) {
+                if (entity.getDefinition() == EntityDefinitions.SHEEP) {
                     entityEventPacket.setType(EntityEventType.EAT_GRASS);
                 } else {
                     entityEventPacket.setType(EntityEventType.PRIME_TNT_MINECART);
@@ -182,12 +182,12 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
                 entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_WITHDRAW);
                 break;
             case IRON_GOLEM_ATTACK:
-                if (entity.getEntityType() == EntityType.IRON_GOLEM) {
+                if (entity.getDefinition() == EntityDefinitions.IRON_GOLEM) {
                     entityEventPacket.setType(EntityEventType.ATTACK_START);
                 }
                 break;
             case RABBIT_JUMP_OR_MINECART_SPAWNER_DELAY_RESET:
-                if (entity.getEntityType() == EntityType.RABBIT) {
+                if (entity.getDefinition() == EntityDefinitions.RABBIT) {
                     // This doesn't match vanilla Bedrock behavior but I'm unsure how to make it better
                     // I assume part of the problem is that Bedrock uses a duration and Java just says the rabbit is jumping
                     SetEntityDataPacket dataPacket = new SetEntityDataPacket();
@@ -223,12 +223,12 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
                 }
                 return;
             case GOAT_LOWERING_HEAD:
-                if (entity.getEntityType() == EntityType.GOAT) {
+                if (entity.getDefinition() == EntityDefinitions.GOAT) {
                     entityEventPacket.setType(EntityEventType.ATTACK_START);
                 }
                 break;
             case GOAT_STOP_LOWERING_HEAD:
-                if (entity.getEntityType() == EntityType.GOAT) {
+                if (entity.getDefinition() == EntityDefinitions.GOAT) {
                     entityEventPacket.setType(EntityEventType.ATTACK_STOP);
                 }
                 break;
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosRotTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosRotTranslator.java
index ec9fde618..4dffd4c26 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosRotTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosRotTranslator.java
@@ -42,6 +42,6 @@ public class JavaMoveEntityPosRotTranslator extends PacketTranslator<Clientbound
         }
         if (entity == null) return;
 
-        entity.updatePositionAndRotation(session, packet.getMoveX(), packet.getMoveY(), packet.getMoveZ(), packet.getYaw(), packet.getPitch(), packet.isOnGround());
+        entity.updatePositionAndRotation(packet.getMoveX(), packet.getMoveY(), packet.getMoveZ(), packet.getYaw(), packet.getPitch(), packet.isOnGround());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosTranslator.java
index e21476a6c..158c8c8d0 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityPosTranslator.java
@@ -44,6 +44,6 @@ public class JavaMoveEntityPosTranslator extends PacketTranslator<ClientboundMov
         }
         if (entity == null) return;
 
-        entity.moveRelative(session, packet.getMoveX(), packet.getMoveY(), packet.getMoveZ(), entity.getRotation(), packet.isOnGround());
+        entity.moveRelative(packet.getMoveX(), packet.getMoveY(), packet.getMoveZ(), entity.getYaw(), entity.getPitch(), entity.getHeadYaw(), packet.isOnGround());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityRotTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityRotTranslator.java
index 36bd6741e..519b30909 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityRotTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveEntityRotTranslator.java
@@ -42,6 +42,6 @@ public class JavaMoveEntityRotTranslator extends PacketTranslator<ClientboundMov
         }
         if (entity == null) return;
 
-        entity.updateRotation(session, packet.getYaw(), packet.getPitch(), packet.isOnGround());
+        entity.updateRotation(packet.getYaw(), packet.getPitch(), packet.isOnGround());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveVehicleTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveVehicleTranslator.java
index 28a007931..cb15572a1 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveVehicleTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaMoveVehicleTranslator.java
@@ -40,6 +40,6 @@ public class JavaMoveVehicleTranslator extends PacketTranslator<ClientboundMoveV
         Entity entity = session.getRidingVehicleEntity();
         if (entity == null) return;
 
-        entity.moveAbsolute(session, Vector3f.from(packet.getX(), packet.getY(), packet.getZ()), packet.getYaw(), packet.getPitch(), false, true);
+        entity.moveAbsolute(Vector3f.from(packet.getX(), packet.getY(), packet.getZ()), packet.getYaw(), packet.getPitch(), false, true);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaRotateHeadTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaRotateHeadTranslator.java
index 3ad40abd0..57c34db4f 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaRotateHeadTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaRotateHeadTranslator.java
@@ -45,6 +45,6 @@ public class JavaRotateHeadTranslator extends PacketTranslator<ClientboundRotate
 
         if (entity == null) return;
 
-        entity.updateHeadLookRotation(session, packet.getHeadYaw());
+        entity.updateHeadLookRotation(packet.getHeadYaw());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityDataTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityDataTranslator.java
index 1172509b9..f2461c5e9 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityDataTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityDataTranslator.java
@@ -28,12 +28,15 @@ package org.geysermc.connector.network.translators.java.entity;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityDataPacket;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityMetadataTranslator;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
 import org.geysermc.connector.utils.InteractiveTagManager;
 import org.geysermc.connector.utils.LanguageUtils;
 
+import java.util.List;
+
 @Translator(packet = ClientboundSetEntityDataPacket.class)
 public class JavaSetEntityDataTranslator extends PacketTranslator<ClientboundSetEntityDataPacket> {
 
@@ -47,22 +50,30 @@ public class JavaSetEntityDataTranslator extends PacketTranslator<ClientboundSet
         }
         if (entity == null) return;
 
-        for (EntityMetadata metadata : packet.getMetadata()) {
-            try {
-                entity.updateBedrockMetadata(metadata, session);
-            } catch (ClassCastException e) {
-                // Class cast exceptions are really the only ones we're going to get in normal gameplay
-                // Because some entity rewriters forget about some values
-                // Any other errors are actual bugs
-                session.getConnector().getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.translator.metadata.failed", metadata, entity.getEntityType()));
-                session.getConnector().getLogger().debug("Entity Java ID: " + entity.getEntityId() + ", Geyser ID: " + entity.getGeyserId());
+        List<EntityMetadataTranslator<?, ?>> translators = (List<EntityMetadataTranslator<?, ?>>) entity.getDefinition().translators();
+
+        for (EntityMetadata<?> metadata : packet.getMetadata()) {
+            if (metadata.getId() >= translators.size()) {
+                session.getConnector().getLogger().warning("Metadata ID " + metadata.getId() + " is out of bounds of known entity metadata size " + translators.size() + " for entity type " + entity.getDefinition().entityType());
                 if (session.getConnector().getConfig().isDebugMode()) {
-                    e.printStackTrace();
+                    session.getConnector().getLogger().debug(metadata.toString());
                 }
+                continue;
             }
+
+            EntityMetadataTranslator<? super Entity, ?> translator = (EntityMetadataTranslator<? super Entity, ?>) translators.get(metadata.getId());
+            if (translator == null) {
+                // This can safely happen; it means we don't translate this entity metadata
+                continue;
+            }
+            if (translator.acceptedType() != metadata.getType()) {
+                session.getConnector().getLogger().warning("Metadata ID " + metadata.getId() + " was received with type " + metadata.getType() + " but we expected " + translator.acceptedType() + " for " + entity.getDefinition().entityType());
+                continue;
+            }
+            translator.translateFunction().accept(entity, metadata);
         }
 
-        entity.updateBedrockMetadata(session);
+        entity.updateBedrockMetadata();
 
         // Update the interactive tag, if necessary
         if (session.getMouseoverEntity() != null && session.getMouseoverEntity().getEntityId() == entity.getEntityId()) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityLinkTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityLinkTranslator.java
index 0fd9b0c64..27e0481ee 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityLinkTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEntityLinkTranslator.java
@@ -26,11 +26,11 @@
 package org.geysermc.connector.network.translators.java.entity;
 
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityLinkPacket;
-import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.living.MobEntity;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -43,15 +43,14 @@ public class JavaSetEntityLinkTranslator extends PacketTranslator<ClientboundSet
 
     @Override
     public void translate(GeyserSession session, ClientboundSetEntityLinkPacket packet) {
-
         Entity holderId;
         if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
             holderId = session.getPlayerEntity();
         } else {
             holderId = session.getEntityCache().getEntityByJavaId(packet.getEntityId());
-            if (holderId == null) {
-                return;
-            }
+        }
+        if (!(holderId instanceof MobEntity mobEntity)) {
+            return;
         }
 
         Entity attachedToId;
@@ -61,9 +60,9 @@ public class JavaSetEntityLinkTranslator extends PacketTranslator<ClientboundSet
             attachedToId = session.getEntityCache().getEntityByJavaId(packet.getAttachedToId());
             if ((attachedToId == null || packet.getAttachedToId() == 0)) {
                 // Is not being leashed
-                holderId.getMetadata().getFlags().setFlag(EntityFlag.LEASHED, false);
-                holderId.getMetadata().put(EntityData.LEASH_HOLDER_EID, -1L);
-                holderId.updateBedrockMetadata(session);
+                mobEntity.setFlag(EntityFlag.LEASHED, false);
+                mobEntity.setLeashHolderBedrockId(-1L);
+                mobEntity.updateBedrockMetadata();
                 EntityEventPacket eventPacket = new EntityEventPacket();
                 eventPacket.setRuntimeEntityId(holderId.getGeyserId());
                 eventPacket.setType(EntityEventType.REMOVE_LEASH);
@@ -73,8 +72,8 @@ public class JavaSetEntityLinkTranslator extends PacketTranslator<ClientboundSet
             }
         }
 
-        holderId.getMetadata().getFlags().setFlag(EntityFlag.LEASHED, true);
-        holderId.getMetadata().put(EntityData.LEASH_HOLDER_EID, attachedToId.getGeyserId());
-        holderId.updateBedrockMetadata(session);
+        mobEntity.setFlag(EntityFlag.LEASHED, true);
+        mobEntity.setLeashHolderBedrockId(attachedToId.getGeyserId());
+        holderId.updateBedrockMetadata();
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEquipmentTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEquipmentTranslator.java
index 2b5cceb37..9e84b85a1 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEquipmentTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetEquipmentTranslator.java
@@ -52,7 +52,7 @@ public class JavaSetEquipmentTranslator extends PacketTranslator<ClientboundSetE
 
         if (!(entity instanceof LivingEntity livingEntity)) {
             session.getConnector().getLogger().debug("Attempted to add armor to a non-living entity type (" +
-                    entity.getEntityType().name() + ").");
+                    entity.getDefinition().entityType().name() + ").");
             return;
         }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetPassengersTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetPassengersTranslator.java
index dc6553492..1fc50cbb6 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetPassengersTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaSetPassengersTranslator.java
@@ -31,7 +31,7 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData;
 import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket;
 import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 import org.geysermc.connector.entity.Entity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -61,7 +61,7 @@ public class JavaSetPassengersTranslator extends PacketTranslator<ClientboundSet
                 passenger = session.getPlayerEntity();
                 session.setRidingVehicleEntity(entity);
                 // We need to confirm teleports before entering a vehicle, or else we will likely exit right out
-                session.confirmTeleport(passenger.getPosition().sub(0, EntityType.PLAYER.getOffset(), 0).toDouble());
+                session.confirmTeleport(passenger.getPosition().sub(0, EntityDefinitions.PLAYER.offset(), 0).toDouble());
             }
             // Passenger hasn't loaded in (likely since we're waiting for a skin response)
             // and entity link needs to be set later
@@ -79,18 +79,18 @@ public class JavaSetPassengersTranslator extends PacketTranslator<ClientboundSet
             passengers.add(passengerId);
 
             // Head rotation on boats
-            if (entity.getEntityType() == EntityType.BOAT) {
-                passenger.getMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 1);
-                passenger.getMetadata().put(EntityData.RIDER_MAX_ROTATION, 90f);
-                passenger.getMetadata().put(EntityData.RIDER_MIN_ROTATION, 1f);
-                passenger.getMetadata().put(EntityData.RIDER_ROTATION_OFFSET, -90f);
+            if (entity.getDefinition() == EntityDefinitions.BOAT) {
+                passenger.getDirtyMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 1);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_MAX_ROTATION, 90f);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_MIN_ROTATION, 1f);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_ROTATION_OFFSET, -90f);
             } else {
-                passenger.getMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 0);
-                passenger.getMetadata().put(EntityData.RIDER_MAX_ROTATION, 0f);
-                passenger.getMetadata().put(EntityData.RIDER_MIN_ROTATION, 0f);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 0);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_MAX_ROTATION, 0f);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_MIN_ROTATION, 0f);
             }
 
-            passenger.updateBedrockMetadata(session);
+            passenger.updateBedrockMetadata();
             rider = false;
         }
 
@@ -109,24 +109,24 @@ public class JavaSetPassengersTranslator extends PacketTranslator<ClientboundSet
                 linkPacket.setEntityLink(new EntityLinkData(entity.getGeyserId(), passenger.getGeyserId(), EntityLinkData.Type.REMOVE, false));
                 session.sendUpstreamPacket(linkPacket);
                 passengers.remove(passenger.getEntityId());
-                passenger.getMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 0);
-                passenger.getMetadata().put(EntityData.RIDER_MAX_ROTATION, 0f);
-                passenger.getMetadata().put(EntityData.RIDER_MIN_ROTATION, 0f);
-                passenger.getMetadata().put(EntityData.RIDER_ROTATION_OFFSET, 0f);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 0);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_MAX_ROTATION, 0f);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_MIN_ROTATION, 0f);
+                passenger.getDirtyMetadata().put(EntityData.RIDER_ROTATION_OFFSET, 0f);
 
-                EntityUtils.updateMountOffset(passenger, entity, session, false, false, (packet.getPassengerIds().length > 1));
+                EntityUtils.updateMountOffset(passenger, entity, false, false, (packet.getPassengerIds().length > 1));
             } else {
-                EntityUtils.updateMountOffset(passenger, entity, session, (packet.getPassengerIds()[0] == passengerId), true, (packet.getPassengerIds().length > 1));
+                EntityUtils.updateMountOffset(passenger, entity, (packet.getPassengerIds()[0] == passengerId), true, (packet.getPassengerIds().length > 1));
             }
 
             // Force an update to the passenger metadata
-            passenger.updateBedrockMetadata(session);
+            passenger.updateBedrockMetadata();
         }
 
-        switch (entity.getEntityType()) {
+        switch (entity.getDefinition().entityType()) {
             case HORSE, SKELETON_HORSE, DONKEY, MULE, RAVAGER -> {
-                entity.getMetadata().put(EntityData.RIDER_MAX_ROTATION, 181.0f);
-                entity.updateBedrockMetadata(session);
+                entity.getDirtyMetadata().put(EntityData.RIDER_MAX_ROTATION, 181.0f);
+                entity.updateBedrockMetadata();
             }
         }
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaTeleportEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaTeleportEntityTranslator.java
index 8fbd234a9..89bcb5c0e 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaTeleportEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaTeleportEntityTranslator.java
@@ -43,6 +43,6 @@ public class JavaTeleportEntityTranslator extends PacketTranslator<ClientboundTe
         }
         if (entity == null) return;
 
-        entity.teleport(session, Vector3f.from(packet.getX(), packet.getY(), packet.getZ()), packet.getYaw(), packet.getPitch(), packet.isOnGround());
+        entity.teleport(Vector3f.from(packet.getX(), packet.getY(), packet.getZ()), packet.getYaw(), packet.getPitch(), packet.isOnGround());
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerInfoTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerInfoTranslator.java
index 46170076e..d7bfd5d72 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerInfoTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerInfoTranslator.java
@@ -63,12 +63,13 @@ public class JavaPlayerInfoTranslator extends PacketTranslator<ClientboundPlayer
                     if (playerEntity == null) {
                         // It's a new player
                         playerEntity = new PlayerEntity(
-                                entry.getProfile(),
+                                session,
                                 -1,
                                 session.getEntityCache().getNextEntityId().incrementAndGet(),
+                                entry.getProfile(),
                                 Vector3f.ZERO,
                                 Vector3f.ZERO,
-                                Vector3f.ZERO
+                                0, 0, 0
                         );
 
                         session.getEntityCache().addPlayerEntity(playerEntity);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionTranslator.java
index 1c3866adf..f16c4aadf 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionTranslator.java
@@ -36,8 +36,8 @@ import com.nukkitx.protocol.bedrock.packet.RespawnPacket;
 import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket;
 import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.player.PlayerEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.session.cache.TeleportCache;
 import org.geysermc.connector.network.translators.PacketTranslator;
@@ -59,7 +59,9 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
         if (!session.isSpawned()) {
             Vector3f pos = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
             entity.setPosition(pos);
-            entity.setRotation(Vector3f.from(packet.getYaw(), packet.getPitch(), packet.getYaw()));
+            entity.setYaw(packet.getYaw());
+            entity.setPitch(packet.getPitch());
+            entity.setHeadYaw(packet.getYaw());
 
             RespawnPacket respawnPacket = new RespawnPacket();
             respawnPacket.setRuntimeEntityId(0); // Bedrock server behavior
@@ -69,7 +71,7 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
 
             SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
             entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
-            entityDataPacket.getMetadata().putAll(entity.getMetadata());
+            entityDataPacket.getMetadata().putAll(entity.getDirtyMetadata());
             session.sendUpstreamPacket(entityDataPacket);
 
             MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
@@ -98,21 +100,21 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
             linkPacket.setEntityLink(new EntityLinkData(vehicle.getGeyserId(), entity.getGeyserId(), EntityLinkData.Type.REMOVE, false, false));
             session.sendUpstreamPacket(linkPacket);
             vehicle.getPassengers().remove(entity.getEntityId());
-            entity.getMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 0);
-            entity.getMetadata().put(EntityData.RIDER_MAX_ROTATION, 0f);
-            entity.getMetadata().put(EntityData.RIDER_MIN_ROTATION, 0f);
-            entity.getMetadata().put(EntityData.RIDER_ROTATION_OFFSET, 0f);
+            entity.getDirtyMetadata().put(EntityData.RIDER_ROTATION_LOCKED, (byte) 0);
+            entity.getDirtyMetadata().put(EntityData.RIDER_MAX_ROTATION, 0f);
+            entity.getDirtyMetadata().put(EntityData.RIDER_MIN_ROTATION, 0f);
+            entity.getDirtyMetadata().put(EntityData.RIDER_ROTATION_OFFSET, 0f);
             session.setRidingVehicleEntity(null);
-            entity.updateBedrockMetadata(session);
+            entity.updateBedrockMetadata();
 
-            EntityUtils.updateMountOffset(entity, vehicle, session, false, false, entity.getPassengers().size() > 1);
+            EntityUtils.updateMountOffset(entity, vehicle, false, false, entity.getPassengers().size() > 1);
         }
 
         // If coordinates are relative, then add to the existing coordinate
         double newX = packet.getX() +
                 (packet.getRelative().contains(PositionElement.X) ? entity.getPosition().getX() : 0);
         double newY = packet.getY() +
-                (packet.getRelative().contains(PositionElement.Y) ? entity.getPosition().getY() - EntityType.PLAYER.getOffset() : 0);
+                (packet.getRelative().contains(PositionElement.Y) ? entity.getPosition().getY() - EntityDefinitions.PLAYER.offset() : 0);
         double newZ = packet.getZ() +
                 (packet.getRelative().contains(PositionElement.Z) ? entity.getPosition().getZ() : 0);
 
@@ -121,16 +123,16 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
         float newYaw = packet.getYaw() +
                 (packet.getRelative().contains(PositionElement.YAW) ? entity.getBedrockRotation().getY() : 0);
 
-        session.getConnector().getLogger().debug("Teleport from " + entity.getPosition().getX() + " " + (entity.getPosition().getY() - EntityType.PLAYER.getOffset()) + " " + entity.getPosition().getZ());
+        session.getConnector().getLogger().debug("Teleport from " + entity.getPosition().getX() + " " + (entity.getPosition().getY() - EntityDefinitions.PLAYER.offset()) + " " + entity.getPosition().getZ());
 
         session.addTeleport(new TeleportCache(newX, newY, newZ, newPitch, newYaw, packet.getTeleportId()));
 
-        Vector3f lastPlayerPosition = entity.getPosition().down(EntityType.PLAYER.getOffset());
+        Vector3f lastPlayerPosition = entity.getPosition().down(EntityDefinitions.PLAYER.offset());
         float lastPlayerPitch = entity.getBedrockRotation().getX();
         Vector3f teleportDestination = Vector3f.from(newX, newY, newZ);
-        entity.moveAbsolute(session, teleportDestination, newYaw, newPitch, true, true);
+        entity.moveAbsolute(teleportDestination, newYaw, newPitch, true, true);
 
-        session.getConnector().getLogger().debug("to " + entity.getPosition().getX() + " " + (entity.getPosition().getY() - EntityType.PLAYER.getOffset()) + " " + entity.getPosition().getZ());
+        session.getConnector().getLogger().debug("to " + entity.getPosition().getX() + " " + (entity.getPosition().getY() - EntityDefinitions.PLAYER.offset()) + " " + entity.getPosition().getZ());
 
         // Bedrock ignores teleports that are extremely close to the player's original position and orientation,
         // so check if we can immediately confirm the teleport
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddEntityTranslator.java
index fb22e2cc2..cf75dbd0f 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddEntityTranslator.java
@@ -25,78 +25,65 @@
 
 package org.geysermc.connector.network.translators.java.entity.spawn;
 
+import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.object.FallingBlockData;
-import com.github.steveice10.mc.protocol.data.game.entity.object.HangingDirection;
 import com.github.steveice10.mc.protocol.data.game.entity.object.ProjectileData;
 import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddEntityPacket;
 import com.nukkitx.math.vector.Vector3f;
 import org.geysermc.connector.entity.*;
+import org.geysermc.connector.entity.factory.BaseEntityFactory;
 import org.geysermc.connector.entity.player.PlayerEntity;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
-import org.geysermc.connector.utils.EntityUtils;
+import org.geysermc.connector.registry.Registries;
 import org.geysermc.connector.utils.LanguageUtils;
 
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-
 @Translator(packet = ClientboundAddEntityPacket.class)
 public class JavaAddEntityTranslator extends PacketTranslator<ClientboundAddEntityPacket> {
 
     @Override
     public void translate(GeyserSession session, ClientboundAddEntityPacket packet) {
-
         Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
         Vector3f motion = Vector3f.from(packet.getMotionX(), packet.getMotionY(), packet.getMotionZ());
-        Vector3f rotation = Vector3f.from(packet.getYaw(), packet.getPitch(), 0);
+        float yaw = packet.getYaw();
+        float pitch = packet.getPitch();
 
-        org.geysermc.connector.entity.type.EntityType type = EntityUtils.toBedrockEntity(packet.getType());
-        if (type == null) {
+        EntityDefinition<?> definition = Registries.ENTITY_DEFINITIONS.get(packet.getType());
+        if (definition == null) {
             session.getConnector().getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.entity.type_null", packet.getType()));
             return;
         }
 
-        Class<? extends Entity> entityClass = type.getEntityClass();
-        try {
-            Entity entity;
-            if (packet.getType() == EntityType.FALLING_BLOCK) {
-                entity = new FallingBlockEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                        type, position, motion, rotation, ((FallingBlockData) packet.getData()).getId());
-            } else if (packet.getType() == EntityType.ITEM_FRAME || packet.getType() == EntityType.GLOW_ITEM_FRAME) {
-                // Item frames need the hanging direction
-                entity = new ItemFrameEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                        type, position, motion, rotation, (HangingDirection) packet.getData());
-            } else if (packet.getType() == EntityType.FISHING_BOBBER) {
-                // Fishing bobbers need the owner for the line
-                int ownerEntityId = ((ProjectileData) packet.getData()).getOwnerId();
-                Entity owner = session.getEntityCache().getEntityByJavaId(ownerEntityId);
-                if (owner == null && session.getPlayerEntity().getEntityId() == ownerEntityId) {
-                    owner = session.getPlayerEntity();
-                }
-                // Java clients only spawn fishing hooks with a player as its owner
-                if (owner instanceof PlayerEntity) {
-                    entity = new FishingHookEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                            type, position, motion, rotation, (PlayerEntity) owner);
-                } else {
-                    return;
-                }
-            } else if (packet.getType() == EntityType.BOAT) {
-                // Initial rotation is incorrect
-                entity = new BoatEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                        type, position, motion, Vector3f.from(packet.getYaw(), 0, packet.getYaw()));
+        Entity entity;
+        if (packet.getType() == EntityType.FALLING_BLOCK) {
+            entity = new FallingBlockEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(),
+                    position, motion, yaw, pitch, ((FallingBlockData) packet.getData()).getId());
+        } else if (packet.getType() == EntityType.ITEM_FRAME || packet.getType() == EntityType.GLOW_ITEM_FRAME) {
+            // Item frames need the hanging direction
+            entity = new ItemFrameEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(),
+                    definition, position, motion, yaw, pitch, (Direction) packet.getData());
+        } else if (packet.getType() == EntityType.FISHING_BOBBER) {
+            // Fishing bobbers need the owner for the line
+            int ownerEntityId = ((ProjectileData) packet.getData()).getOwnerId();
+            Entity owner;
+            if (session.getPlayerEntity().getEntityId() == ownerEntityId) {
+                owner = session.getPlayerEntity();
             } else {
-                Constructor<? extends Entity> entityConstructor = entityClass.getConstructor(long.class, long.class, org.geysermc.connector.entity.type.EntityType.class,
-                        Vector3f.class, Vector3f.class, Vector3f.class);
-
-                entity = entityConstructor.newInstance(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                        type, position, motion, rotation
-                );
+                owner = session.getEntityCache().getEntityByJavaId(ownerEntityId);
             }
-            session.getEntityCache().spawnEntity(entity);
-        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
-            ex.printStackTrace();
+            // Java clients only spawn fishing hooks with a player as its owner
+            if (owner instanceof PlayerEntity) {
+                entity = new FishingHookEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(),
+                        position, motion, yaw, pitch, (PlayerEntity) owner);
+            } else {
+                return;
+            }
+        } else {
+            entity = ((BaseEntityFactory<?>) definition.factory()).create(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
+                    packet.getUuid(), definition, position, motion, yaw, pitch, 0f);
         }
+        session.getEntityCache().spawnEntity(entity);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddExperienceOrbTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddExperienceOrbTranslator.java
index 263170f95..ec2ab1221 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddExperienceOrbTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddExperienceOrbTranslator.java
@@ -29,7 +29,6 @@ import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.spawn.
 import com.nukkitx.math.vector.Vector3f;
 import org.geysermc.connector.entity.Entity;
 import org.geysermc.connector.entity.ExpOrbEntity;
-import org.geysermc.connector.entity.type.EntityType;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
@@ -42,8 +41,7 @@ public class JavaAddExperienceOrbTranslator extends PacketTranslator<Clientbound
         Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
 
         Entity entity = new ExpOrbEntity(
-                packet.getExp(), packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                EntityType.EXPERIENCE_ORB, position, Vector3f.ZERO, Vector3f.ZERO
+                session, packet.getExp(), packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), position
         );
 
         session.getEntityCache().spawnEntity(entity);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddMobTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddMobTranslator.java
index f1fe12c26..a3d7ebb0d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddMobTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddMobTranslator.java
@@ -28,16 +28,14 @@ package org.geysermc.connector.network.translators.java.entity.spawn;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddMobPacket;
 import com.nukkitx.math.vector.Vector3f;
 import org.geysermc.connector.entity.Entity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.entity.factory.BaseEntityFactory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
-import org.geysermc.connector.utils.EntityUtils;
+import org.geysermc.connector.registry.Registries;
 import org.geysermc.connector.utils.LanguageUtils;
 
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-
 @Translator(packet = ClientboundAddMobPacket.class)
 public class JavaAddMobTranslator extends PacketTranslator<ClientboundAddMobPacket> {
 
@@ -45,25 +43,16 @@ public class JavaAddMobTranslator extends PacketTranslator<ClientboundAddMobPack
     public void translate(GeyserSession session, ClientboundAddMobPacket packet) {
         Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
         Vector3f motion = Vector3f.from(packet.getMotionX(), packet.getMotionY(), packet.getMotionZ());
-        Vector3f rotation = Vector3f.from(packet.getYaw(), packet.getPitch(), packet.getHeadYaw());
 
-        EntityType type = EntityUtils.toBedrockEntity(packet.getType());
-        if (type == null) {
+        EntityDefinition<?> definition = Registries.ENTITY_DEFINITIONS.get(packet.getType());
+        if (definition == null) {
             session.getConnector().getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.entity.type_null", packet.getType()));
             return;
         }
 
-        Class<? extends Entity> entityClass = type.getEntityClass();
-        try {
-            Constructor<? extends Entity> entityConstructor = entityClass.getConstructor(long.class, long.class, EntityType.class,
-                    Vector3f.class, Vector3f.class, Vector3f.class);
-
-            Entity entity = entityConstructor.newInstance(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                    type, position, motion, rotation
-            );
-            session.getEntityCache().spawnEntity(entity);
-        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
-            ex.printStackTrace();
-        }
+        Entity entity = ((BaseEntityFactory<?>) definition.factory()).create(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
+                packet.getUuid(), definition, position, motion, packet.getYaw(), packet.getPitch(), packet.getHeadYaw()
+        );
+        session.getEntityCache().spawnEntity(entity);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPaintingTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPaintingTranslator.java
index 790e79ecf..4879166a2 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPaintingTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPaintingTranslator.java
@@ -40,9 +40,9 @@ public class JavaAddPaintingTranslator extends PacketTranslator<ClientboundAddPa
     public void translate(GeyserSession session, ClientboundAddPaintingPacket packet) {
         Vector3f position = Vector3f.from(packet.getPosition().getX(), packet.getPosition().getY(), packet.getPosition().getZ());
 
-        PaintingEntity entity = new PaintingEntity(packet.getEntityId(),
-                session.getEntityCache().getNextEntityId().incrementAndGet(),
-                position, PaintingType.getByPaintingType(packet.getPaintingType()), packet.getDirection().ordinal());
+        PaintingEntity entity = new PaintingEntity(session, packet.getEntityId(),
+                session.getEntityCache().getNextEntityId().incrementAndGet(), packet.getUuid(),
+                position, PaintingType.getByPaintingType(packet.getPaintingType()), packet.getDirection().getHorizontalIndex());
 
         session.getEntityCache().spawnEntity(entity);
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPlayerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPlayerTranslator.java
index 0cf689f77..9daec1007 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPlayerTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaAddPlayerTranslator.java
@@ -41,12 +41,14 @@ public class JavaAddPlayerTranslator extends PacketTranslator<ClientboundAddPlay
     @Override
     public void translate(GeyserSession session, ClientboundAddPlayerPacket packet) {
         Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
-        Vector3f rotation = Vector3f.from(packet.getYaw(), packet.getPitch(), packet.getYaw());
+        float yaw = packet.getYaw();
+        float pitch = packet.getPitch();
+        float headYaw = packet.getYaw();
 
         PlayerEntity entity;
         if (packet.getUuid().equals(session.getPlayerEntity().getUuid())) {
             // Server is sending a fake version of the current player
-            entity = new PlayerEntity(session.getPlayerEntity().getProfile(), packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), position, Vector3f.ZERO, rotation);
+            entity = new PlayerEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), session.getPlayerEntity().getProfile(), position, Vector3f.ZERO, yaw, pitch, headYaw);
         } else {
             entity = session.getEntityCache().getPlayerEntity(packet.getUuid());
             if (entity == null) {
@@ -56,11 +58,13 @@ public class JavaAddPlayerTranslator extends PacketTranslator<ClientboundAddPlay
 
             entity.setEntityId(packet.getEntityId());
             entity.setPosition(position);
-            entity.setRotation(rotation);
+            entity.setYaw(yaw);
+            entity.setPitch(pitch);
+            entity.setHeadYaw(headYaw);
         }
         session.getEntityCache().cacheEntity(entity);
 
-        entity.sendPlayer(session);
+        entity.sendPlayer();
         SkinManager.requestAndHandleSkinAndCape(entity, session, null);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaHorseScreenOpenTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaHorseScreenOpenTranslator.java
index 7eb00ea65..831d50be1 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaHorseScreenOpenTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaHorseScreenOpenTranslator.java
@@ -132,6 +132,6 @@ public class JavaHorseScreenOpenTranslator extends PacketTranslator<ClientboundH
         session.sendUpstreamPacket(updateEquipPacket);
 
         session.setInventoryTranslator(inventoryTranslator);
-        InventoryUtils.openInventory(session, new Container(entity.getMetadata().getString(EntityData.NAMETAG), packet.getContainerId(), packet.getNumberOfSlots(), null, session.getPlayerInventory()));
+        InventoryUtils.openInventory(session, new Container(entity.getDirtyMetadata().getString(EntityData.NAMETAG), packet.getContainerId(), packet.getNumberOfSlots(), null, session.getPlayerInventory()));
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaMerchantOffersTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaMerchantOffersTranslator.java
index 7d8b2f147..61b8471c6 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaMerchantOffersTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/inventory/JavaMerchantOffersTranslator.java
@@ -60,10 +60,10 @@ public class JavaMerchantOffersTranslator extends PacketTranslator<ClientboundMe
         // Retrieve the fake villager involved in the trade, and update its metadata to match with the window information
         merchantInventory.setVillagerTrades(packet.getTrades());
         Entity villager = merchantInventory.getVillager();
-        villager.getMetadata().put(EntityData.TRADE_TIER, packet.getVillagerLevel() - 1);
-        villager.getMetadata().put(EntityData.MAX_TRADE_TIER, 4);
-        villager.getMetadata().put(EntityData.TRADE_XP, packet.getExperience());
-        villager.updateBedrockMetadata(session);
+        villager.getDirtyMetadata().put(EntityData.TRADE_TIER, packet.getVillagerLevel() - 1);
+        villager.getDirtyMetadata().put(EntityData.MAX_TRADE_TIER, 4);
+        villager.getDirtyMetadata().put(EntityData.TRADE_XP, packet.getExperience());
+        villager.updateBedrockMetadata();
 
         // Construct the packet that opens the trading window
         UpdateTradePacket updateTradePacket = new UpdateTradePacket();
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaForgetLevelChunkTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaForgetLevelChunkTranslator.java
index 696ec3d92..aba3c746a 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaForgetLevelChunkTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaForgetLevelChunkTranslator.java
@@ -45,7 +45,7 @@ public class JavaForgetLevelChunkTranslator extends PacketTranslator<Clientbound
         while (iterator.hasNext()) {
             Vector3i position = iterator.next();
             if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
-                session.getSkullCache().get(position).despawnEntity(session);
+                session.getSkullCache().get(position).despawnEntity();
                 iterator.remove();
             }
         }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetObjectiveTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetObjectiveTranslator.java
index b74d58175..8c1ab9124 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetObjectiveTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetObjectiveTranslator.java
@@ -73,7 +73,7 @@ public class JavaSetObjectiveTranslator extends PacketTranslator<ClientboundSetO
                         // Other places we check for the entity being valid,
                         // but we must set the below name text as null for all players
                         // or else PlayerEntity#spawnEntity will find a null objective and not touch EntityData#SCORE_TAG
-                        entity.setBelowNameText(session, null);
+                        entity.setBelowNameText(null);
                     }
                 }
             }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetScoreTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetScoreTranslator.java
index 7b37dc455..76e64490b 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetScoreTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaSetScoreTranslator.java
@@ -117,7 +117,7 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
         String displayString = count + " " + objective.getDisplayName();
 
         // Of note: unlike Bedrock, if there is an objective in the below name slot, everyone has a display
-        entity.getMetadata().put(EntityData.SCORE_TAG, displayString);
+        entity.getDirtyMetadata().put(EntityData.SCORE_TAG, displayString);
         SetEntityDataPacket entityDataPacket = new SetEntityDataPacket();
         entityDataPacket.setRuntimeEntityId(entity.getGeyserId());
         entityDataPacket.getMetadata().put(EntityData.SCORE_TAG, displayString);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/sound/EntitySoundInteractionHandler.java b/connector/src/main/java/org/geysermc/connector/network/translators/sound/EntitySoundInteractionHandler.java
index 93611de4e..193b90e1c 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/sound/EntitySoundInteractionHandler.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/sound/EntitySoundInteractionHandler.java
@@ -57,7 +57,7 @@ public interface EntitySoundInteractionHandler extends SoundInteractionHandler<E
             if (interactionEntry.getKey().entities().length != 0) {
                 boolean contains = false;
                 for (String entityIdentifier : interactionEntry.getKey().entities()) {
-                    if (entity.getEntityType().name().toLowerCase().contains(entityIdentifier)) {
+                    if (entity.getDefinition().entityType().name().toLowerCase().contains(entityIdentifier)) {
                         contains = true;
                         break;
                     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/FeedBabySoundInteractionHandler.java b/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/FeedBabySoundInteractionHandler.java
index 609ab61ad..2895ad214 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/FeedBabySoundInteractionHandler.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/FeedBabySoundInteractionHandler.java
@@ -44,7 +44,7 @@ public class FeedBabySoundInteractionHandler implements EntitySoundInteractionHa
     public void handleInteraction(GeyserSession session, Vector3f position, Entity entity) {
         if (entity instanceof AnimalEntity && !(entity instanceof CatEntity || entity instanceof OcelotEntity)) {
             String handIdentifier = session.getPlayerInventory().getItemInHand().getMapping(session).getJavaIdentifier();
-            boolean isBaby = entity.getMetadata().getFlags().getFlag(EntityFlag.BABY);
+            boolean isBaby = entity.getDirtyMetadata().getFlags().getFlag(EntityFlag.BABY);
             if (isBaby && ((AnimalEntity) entity).canEat(session, handIdentifier.replace("minecraft:", ""),
                     session.getPlayerInventory().getItemInHand().getMapping(session))) {
                 // Play the "feed child" effect
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/MilkEntitySoundInteractionHandler.java b/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/MilkEntitySoundInteractionHandler.java
index b8c1b2820..11f929f17 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/MilkEntitySoundInteractionHandler.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/sound/entity/MilkEntitySoundInteractionHandler.java
@@ -43,7 +43,7 @@ public class MilkEntitySoundInteractionHandler implements EntitySoundInteraction
         if (!session.getPlayerInventory().getItemInHand().getMapping(session).getJavaIdentifier().equals("minecraft:bucket")) {
             return;
         }
-        if (value.getMetadata().getFlags().getFlag(EntityFlag.BABY)) {
+        if (value.getDirtyMetadata().getFlags().getFlag(EntityFlag.BABY)) {
             return;
         }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java
index 0dc92bbfe..979cb75df 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java
@@ -108,7 +108,6 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
 
         Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
         Vector3f entityPosition = Vector3f.from(x, y, z);
-        Vector3f entityRotation = Vector3f.from(rotation, 0, rotation);
 
         getProfile(tag).whenComplete((gameProfile, throwable) -> {
             if (gameProfile == null) {
@@ -117,34 +116,34 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
             }
 
             if (session.getEventLoop().inEventLoop()) {
-                spawnPlayer(session, gameProfile, blockPosition, entityPosition, entityRotation, blockState);
+                spawnPlayer(session, gameProfile, blockPosition, entityPosition, rotation, blockState);
             } else {
-                session.executeInEventLoop(() -> spawnPlayer(session, gameProfile, blockPosition, entityPosition, entityRotation, blockState));
+                session.executeInEventLoop(() -> spawnPlayer(session, gameProfile, blockPosition, entityPosition, rotation, blockState));
             }
         });
     }
 
     private static void spawnPlayer(GeyserSession session, GameProfile profile, Vector3i blockPosition,
-                                    Vector3f entityPosition, Vector3f entityRotation, int blockState) {
+                                    Vector3f entityPosition, float rotation, int blockState) {
         long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet();
 
         SkullPlayerEntity existingSkull = session.getSkullCache().get(blockPosition);
         if (existingSkull != null) {
             // Ensure that two skulls can't spawn on the same point
-            existingSkull.despawnEntity(session, blockPosition);
+            existingSkull.despawnEntity(blockPosition);
         }
 
-        SkullPlayerEntity player = new SkullPlayerEntity(profile, geyserId, entityPosition, entityRotation, blockState);
+        SkullPlayerEntity player = new SkullPlayerEntity(session, geyserId, profile, entityPosition, rotation, blockState);
 
         // Cache entity
         session.getSkullCache().put(blockPosition, player);
 
-        player.spawnEntity(session);
+        player.spawnEntity();
 
         SkullSkinManager.requestAndHandleSkin(player, session, (skin -> session.scheduleInEventLoop(() -> {
             // Delay to minimize split-second "player" pop-in
-            player.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false);
-            player.updateBedrockMetadata(session);
+            player.setFlag(EntityFlag.INVISIBLE, false);
+            player.updateBedrockMetadata();
         }, 250, TimeUnit.MILLISECONDS)));
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java
index 579a3196d..88cee13b0 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java
@@ -29,7 +29,8 @@ import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
 import com.github.steveice10.opennbt.tag.builtin.Tag;
 import com.nukkitx.nbt.NbtMapBuilder;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.EntityDefinition;
+import org.geysermc.connector.registry.Registries;
 
 @BlockEntity(type = BlockEntityType.MOB_SPAWNER)
 public class SpawnerBlockEntityTranslator extends BlockEntityTranslator {
@@ -72,10 +73,10 @@ public class SpawnerBlockEntityTranslator extends BlockEntityTranslator {
                     .getValue();
             builder.put("EntityIdentifier", entityID);
 
-            EntityType type = EntityType.getFromIdentifier(entityID);
-            if (type != null) {
-                builder.put("DisplayEntityWidth", type.getWidth());
-                builder.put("DisplayEntityHeight", type.getHeight());
+            EntityDefinition<?> definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityID);
+            if (definition != null) {
+                builder.put("DisplayEntityWidth", definition.width());
+                builder.put("DisplayEntityHeight", definition.height());
                 builder.put("DisplayEntityScale", 1.0f);
             }
         }
diff --git a/connector/src/main/java/org/geysermc/connector/registry/Registries.java b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
index 157d01ea3..15fbeb03f 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/Registries.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
@@ -25,11 +25,12 @@
 
 package org.geysermc.connector.registry;
 
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
 import com.github.steveice10.mc.protocol.data.game.level.event.SoundEvent;
+import com.github.steveice10.mc.protocol.data.game.level.particle.ParticleType;
 import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
 import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
-import com.github.steveice10.mc.protocol.data.game.level.particle.ParticleType;
 import com.nukkitx.nbt.NbtMap;
 import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
 import com.nukkitx.protocol.bedrock.data.inventory.PotionMixData;
@@ -37,12 +38,14 @@ import it.unimi.dsi.fastutil.Pair;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import org.geysermc.connector.entity.EntityDefinition;
 import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
-import org.geysermc.connector.network.translators.world.event.LevelEventTransformer;
 import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
 import org.geysermc.connector.network.translators.sound.SoundHandler;
 import org.geysermc.connector.network.translators.sound.SoundInteractionHandler;
 import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
+import org.geysermc.connector.network.translators.world.event.LevelEventTransformer;
 import org.geysermc.connector.registry.loader.*;
 import org.geysermc.connector.registry.populator.ItemRegistryPopulator;
 import org.geysermc.connector.registry.populator.RecipeRegistryPopulator;
@@ -51,6 +54,7 @@ import org.geysermc.connector.registry.type.ItemMappings;
 import org.geysermc.connector.registry.type.ParticleMapping;
 import org.geysermc.connector.registry.type.SoundMapping;
 
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -59,6 +63,11 @@ import java.util.Set;
  * Holds all the common registries in Geyser.
  */
 public class Registries {
+    /**
+     * A registry holding a CompoundTag of the known entity identifiers.
+     */
+    public static final SimpleRegistry<NbtMap> BEDROCK_ENTITY_IDENTIFIERS = SimpleRegistry.create("bedrock/entity_identifiers.dat", RegistryLoaders.NBT);
+
     /**
      * A registry holding a CompoundTag of all the known biomes.
      */
@@ -90,9 +99,14 @@ public class Registries {
     public static final SimpleMappedRegistry<JavaEnchantment, EnchantmentData> ENCHANTMENTS;
 
     /**
-     * A registry holding a CompoundTag of the known entity identifiers.
+     * A map containing all entity types and their respective Geyser definitions
      */
-    public static final SimpleRegistry<NbtMap> ENTITY_IDENTIFIERS = SimpleRegistry.create("bedrock/entity_identifiers.dat", RegistryLoaders.NBT);
+    public static final SimpleMappedRegistry<EntityType, EntityDefinition<?>> ENTITY_DEFINITIONS = SimpleMappedRegistry.create(RegistryLoaders.empty(() -> new EnumMap<>(EntityType.class)));
+
+    /**
+     * A map containing all Java entity identifiers and their respective Geyser definitions
+     */
+    public static final SimpleMappedRegistry<String, EntityDefinition<?>> JAVA_ENTITY_IDENTIFIERS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new));
 
     /**
      * A versioned registry which holds {@link ItemMappings} for each version. These item mappings contain
diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java
index 682ff8331..7d0cdea7d 100644
--- a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java
+++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java
@@ -117,7 +117,7 @@ public final class Scoreboard {
                     continue;
                 }
 
-                entity.setBelowNameText(session, objective);
+                entity.setBelowNameText(objective);
             }
         }
     }
@@ -368,7 +368,7 @@ public final class Scoreboard {
             for (Entity entity : session.getEntityCache().getEntities().values()) {
                 // This more complex logic is for the future to iterate over all entities, not just players
                 if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) {
-                    player.updateDisplayName(session, team, true);
+                    player.updateDisplayName(team, true);
                     if (names.isEmpty()) {
                         break;
                     }
@@ -384,7 +384,7 @@ public final class Scoreboard {
         for (Entity entity : session.getEntityCache().getEntities().values()) {
             if (entity instanceof PlayerEntity player) {
                 Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
-                player.updateDisplayName(session, playerTeam, true);
+                player.updateDisplayName(playerTeam, true);
             }
         }
     }
diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
index 86b40093e..140577104 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
@@ -196,7 +196,7 @@ public class BlockUtils {
     /**
      * Given a position, return the position if a block were located on the specified block face.
      * @param blockPos the block position
-     * @param face the face of the block - see {@link com.github.steveice10.mc.protocol.data.game.level.block.BlockFace}
+     * @param face the face of the block - see {@link com.github.steveice10.mc.protocol.data.game.entity.object.Direction}
      * @return the block position with the block face accounted for
      */
     public static Vector3i getBlockPosition(Vector3i blockPos, int face) {
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
index 48e1a9622..df2923bb5 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
@@ -152,7 +152,7 @@ public class ChunkUtils {
         ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position);
         if (itemFrameEntity != null) {
             if (blockState == JAVA_AIR_ID) { // Item frame is still present and no block overrides that; refresh it
-                itemFrameEntity.updateBlock(session);
+                itemFrameEntity.updateBlock();
                 // Still update the chunk cache with the new block
                 session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
                 return;
@@ -163,7 +163,7 @@ public class ChunkUtils {
         SkullPlayerEntity skull = session.getSkullCache().get(position);
         if (skull != null && blockState != skull.getBlockState()) {
             // Skull is gone
-            skull.despawnEntity(session, position);
+            skull.despawnEntity(position);
         }
 
         // Prevent moving_piston from being placed
diff --git a/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java b/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java
index fdfb40971..57c0eac73 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/EntityUtils.java
@@ -26,14 +26,14 @@
 package org.geysermc.connector.utils;
 
 import com.github.steveice10.mc.protocol.data.game.entity.Effect;
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.EntityDefinitions;
 import org.geysermc.connector.entity.living.ArmorStandEntity;
 import org.geysermc.connector.entity.living.animal.AnimalEntity;
-import org.geysermc.connector.entity.type.EntityType;
-import org.geysermc.connector.network.session.GeyserSession;
 
 import java.util.Locale;
 
@@ -69,24 +69,10 @@ public final class EntityUtils {
         };
     }
 
-    /**
-     * Converts a MobType to a Bedrock edition EntityType, returns null if the EntityType is not found
-     *
-     * @param type The MobType to convert
-     * @return Converted EntityType
-     */
-    public static EntityType toBedrockEntity(com.github.steveice10.mc.protocol.data.game.entity.type.EntityType type) {
-        try {
-            return EntityType.valueOf(type.name());
-        } catch (IllegalArgumentException ex) {
-            return null;
-        }
-    }
-
     private static float getMountedHeightOffset(Entity mount) {
-        float height = mount.getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT);
+        float height = mount.getDirtyMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT);
         float mountedHeightOffset = height * 0.75f;
-        switch (mount.getEntityType()) {
+        switch (mount.getDefinition().entityType()) {
             case CHICKEN, SPIDER -> mountedHeightOffset = height * 0.5f;
             case DONKEY, MULE -> mountedHeightOffset -= 0.25f;
             case LLAMA -> mountedHeightOffset = height * 0.67f;
@@ -94,7 +80,7 @@ public final class EntityUtils {
                     MINECART_COMMAND_BLOCK -> mountedHeightOffset = 0;
             case BOAT -> mountedHeightOffset = -0.1f;
             case HOGLIN, ZOGLIN -> {
-                boolean isBaby = mount.getMetadata().getFlags().getFlag(EntityFlag.BABY);
+                boolean isBaby = mount.getDirtyMetadata().getFlags().getFlag(EntityFlag.BABY);
                 mountedHeightOffset = height - (isBaby ? 0.2f : 0.15f);
             }
             case PIGLIN -> mountedHeightOffset = height * 0.92f;
@@ -107,7 +93,7 @@ public final class EntityUtils {
 
     private static float getHeightOffset(Entity passenger) {
         boolean isBaby;
-        switch (passenger.getEntityType()) {
+        switch (passenger.getDefinition().entityType()) {
             case SKELETON:
             case STRAY:
             case WITHER_SKELETON:
@@ -124,10 +110,10 @@ public final class EntityUtils {
             case PIGLIN:
             case PIGLIN_BRUTE:
             case ZOMBIFIED_PIGLIN:
-                isBaby = passenger.getMetadata().getFlags().getFlag(EntityFlag.BABY);
+                isBaby = passenger.getDirtyMetadata().getFlags().getFlag(EntityFlag.BABY);
                 return isBaby ? -0.05f : -0.45f;
             case ZOMBIE:
-                isBaby = passenger.getMetadata().getFlags().getFlag(EntityFlag.BABY);
+                isBaby = passenger.getDirtyMetadata().getFlags().getFlag(EntityFlag.BABY);
                 return isBaby ? 0.0f : -0.45f;
             case EVOKER:
             case ILLUSIONER:
@@ -148,8 +134,8 @@ public final class EntityUtils {
     /**
      * Adjust an entity's height if they have mounted/dismounted an entity.
      */
-    public static void updateMountOffset(Entity passenger, Entity mount, GeyserSession session, boolean rider, boolean riding, boolean moreThanOneEntity) {
-        passenger.getMetadata().getFlags().setFlag(EntityFlag.RIDING, riding);
+    public static void updateMountOffset(Entity passenger, Entity mount, boolean rider, boolean riding, boolean moreThanOneEntity) {
+        passenger.setFlag(EntityFlag.RIDING, riding);
         if (riding) {
             // Without the Y offset, Bedrock players will find themselves in the floor when mounting
             float mountedHeightOffset = getMountedHeightOffset(mount);
@@ -158,7 +144,7 @@ public final class EntityUtils {
             float xOffset = 0;
             float yOffset = mountedHeightOffset + heightOffset;
             float zOffset = 0;
-            switch (mount.getEntityType()) {
+            switch (mount.getDefinition().entityType()) {
                 case BOAT:
                     // Without the X offset, more than one entity on a boat is stacked on top of each other
                     if (rider && moreThanOneEntity) {
@@ -180,17 +166,17 @@ public final class EntityUtils {
              * Horses are tinier
              * Players, Minecarts, and Boats have different origins
              */
-            if (passenger.getEntityType() == EntityType.PLAYER && mount.getEntityType() != EntityType.PLAYER) {
-                yOffset += EntityType.PLAYER.getOffset();
+            if (passenger.getDefinition().entityType() == EntityType.PLAYER && mount.getDefinition().entityType() != EntityType.PLAYER) {
+                yOffset += EntityDefinitions.PLAYER.offset();
             }
-            switch (mount.getEntityType()) {
+            switch (mount.getDefinition().entityType()) {
                 case MINECART, MINECART_HOPPER, MINECART_TNT, MINECART_CHEST, MINECART_FURNACE, MINECART_SPAWNER,
-                        MINECART_COMMAND_BLOCK, BOAT -> yOffset -= mount.getEntityType().getHeight() * 0.5f;
+                        MINECART_COMMAND_BLOCK, BOAT -> yOffset -= mount.getDefinition().height() * 0.5f;
             }
             Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset);
-            passenger.getMetadata().put(EntityData.RIDER_SEAT_POSITION, offset);
+            passenger.getDirtyMetadata().put(EntityData.RIDER_SEAT_POSITION, offset);
         }
-        passenger.updateBedrockMetadata(session);
+        passenger.updateBedrockMetadata();
     }
 
     private EntityUtils() {
diff --git a/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java b/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java
index 228a8d448..0f7e66250 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java
@@ -25,14 +25,17 @@
 
 package org.geysermc.connector.utils;
 
+import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import lombok.Getter;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.living.MobEntity;
 import org.geysermc.connector.entity.living.animal.AnimalEntity;
 import org.geysermc.connector.entity.living.animal.horse.HorseEntity;
-import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.entity.living.animal.tameable.CatEntity;
+import org.geysermc.connector.entity.living.animal.tameable.WolfEntity;
+import org.geysermc.connector.entity.living.merchant.VillagerEntity;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.registry.type.ItemMapping;
 
@@ -60,26 +63,26 @@ public class InteractiveTagManager {
      * @param interactEntity the entity that the client is currently facing.
      */
     public static void updateTag(GeyserSession session, Entity interactEntity) {
-        EntityDataMap entityMetadata = interactEntity.getMetadata();
         ItemMapping mapping = session.getPlayerInventory().getItemInHand().getMapping(session);
         String javaIdentifierStripped = mapping.getJavaIdentifier().replace("minecraft:", "");
+        EntityType entityType = interactEntity.getDefinition().entityType();
 
         InteractiveTag interactiveTag = InteractiveTag.NONE;
 
-        if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) {
+        if (interactEntity instanceof MobEntity mobEntity && mobEntity.getLeashHolderBedrockId() == session.getPlayerEntity().getGeyserId()) {
             // Unleash the entity
             interactiveTag = InteractiveTag.REMOVE_LEASH;
-        } else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) &&
-                ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !session.isSneaking()) ||
-                        interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) {
+        } else if (javaIdentifierStripped.equals("saddle") && !interactEntity.getFlag(EntityFlag.SADDLED) &&
+                ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(entityType) && interactEntity.getFlag(EntityFlag.TAMED) && !session.isSneaking()) ||
+                        entityType == EntityType.PIG || entityType == EntityType.STRIDER)) {
             // Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed)
             interactiveTag = InteractiveTag.SADDLE;
         } else if (javaIdentifierStripped.equals("name_tag") && session.getPlayerInventory().getItemInHand().getNbt() != null &&
                 session.getPlayerInventory().getItemInHand().getNbt().contains("display")) {
             // Holding a named name tag
             interactiveTag = InteractiveTag.NAME;
-        } else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) &&
-                entityMetadata.getLong(EntityData.LEASH_HOLDER_EID, -1L) == -1L) {
+        } else if (interactEntity instanceof MobEntity mobEntity &&javaIdentifierStripped.equals("lead")
+                && LEASHABLE_MOB_TYPES.contains(entityType) && mobEntity.getLeashHolderBedrockId() == -1L) {
             // Holding a leash and the mob is leashable for sure
             // (Plugins can change this behavior so that's something to look into in the far far future)
             interactiveTag = InteractiveTag.LEASH;
@@ -87,17 +90,17 @@ public class InteractiveTagManager {
             // This animal can be fed
             interactiveTag = InteractiveTag.FEED;
         } else {
-            switch (interactEntity.getEntityType()) {
+            switch (interactEntity.getDefinition().entityType()) {
                 case BOAT:
                     if (interactEntity.getPassengers().size() < 2) {
                         interactiveTag = InteractiveTag.BOARD_BOAT;
                     }
                     break;
                 case CAT:
-                    if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
-                            entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
+                    if (interactEntity.getFlag(EntityFlag.TAMED) &&
+                            ((CatEntity) interactEntity).getOwnerBedrockId() == session.getPlayerEntity().getGeyserId()) {
                         // Tamed and owned by player - can sit/stand
-                        interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
+                        interactiveTag = interactEntity.getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
                         break;
                     }
                     break;
@@ -128,7 +131,7 @@ public class InteractiveTagManager {
                 case DONKEY:
                 case LLAMA:
                 case MULE:
-                    if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED)
+                    if (interactEntity.getFlag(EntityFlag.TAMED) && !interactEntity.getFlag(EntityFlag.CHESTED)
                             && javaIdentifierStripped.equals("chest")) {
                         // Can attach a chest
                         interactiveTag = InteractiveTag.ATTACH_CHEST;
@@ -139,12 +142,12 @@ public class InteractiveTagManager {
                 case SKELETON_HORSE:
                 case TRADER_LLAMA:
                 case ZOMBIE_HORSE:
-                    boolean tamed = entityMetadata.getFlags().getFlag(EntityFlag.TAMED);
-                    if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || entityMetadata.getFlags().getFlag(EntityFlag.CHESTED))) {
+                    boolean tamed = interactEntity.getFlag(EntityFlag.TAMED);
+                    if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || interactEntity.getFlag(EntityFlag.CHESTED))) {
                         interactiveTag = InteractiveTag.OPEN_CONTAINER;
                         break;
                     }
-                    if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) {
+                    if (!interactEntity.getFlag(EntityFlag.BABY)) {
                         // Can't ride a baby
                         if (tamed) {
                             interactiveTag = InteractiveTag.RIDE_HORSE;
@@ -165,17 +168,17 @@ public class InteractiveTagManager {
                     interactiveTag = InteractiveTag.OPEN_CONTAINER;
                     break;
                 case PIG:
-                    if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
+                    if (interactEntity.getFlag(EntityFlag.SADDLED)) {
                         interactiveTag = InteractiveTag.MOUNT;
                     }
                     break;
                 case PIGLIN:
-                    if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) {
+                    if (!interactEntity.getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) {
                         interactiveTag = InteractiveTag.BARTER;
                     }
                     break;
                 case SHEEP:
-                    if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) {
+                    if (!interactEntity.getFlag(EntityFlag.SHEARED)) {
                         if (javaIdentifierStripped.equals("shears")) {
                             // Shear the sheep
                             interactiveTag = InteractiveTag.SHEAR;
@@ -186,13 +189,13 @@ public class InteractiveTagManager {
                     }
                     break;
                 case STRIDER:
-                    if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
+                    if (interactEntity.getFlag(EntityFlag.SADDLED)) {
                         interactiveTag = InteractiveTag.RIDE_STRIDER;
                     }
                     break;
                 case VILLAGER:
-                    if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0
-                            && entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby
+                    VillagerEntity villager = (VillagerEntity) interactEntity;
+                    if (villager.isCanTradeWith() && !villager.isBaby()) { // Not a nitwit, has a profession and is not a baby
                         interactiveTag = InteractiveTag.TRADE;
                     }
                     break;
@@ -200,13 +203,13 @@ public class InteractiveTagManager {
                     interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably.
                     break;
                 case WOLF:
-                    if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) {
+                    if (javaIdentifierStripped.equals("bone") && !interactEntity.getFlag(EntityFlag.TAMED)) {
                         // Bone and untamed - can tame
                         interactiveTag = InteractiveTag.TAME;
-                    } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
-                            entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
+                    } else if (interactEntity.getFlag(EntityFlag.TAMED) &&
+                            ((WolfEntity) interactEntity).getOwnerBedrockId() == session.getPlayerEntity().getGeyserId()) {
                         // Tamed and owned by player - can sit/stand
-                        interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
+                        interactiveTag = interactEntity.getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
                     }
                     break;
                 case ZOMBIE_VILLAGER:
@@ -219,15 +222,15 @@ public class InteractiveTagManager {
                     break;
             }
         }
-        session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue());
-        session.getPlayerEntity().updateBedrockMetadata(session);
+        session.getPlayerEntity().getDirtyMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue());
+        session.getPlayerEntity().updateBedrockMetadata();
     }
 
     /**
      * All interactive tags in enum form. For potential API usage.
      */
     public enum InteractiveTag {
-        NONE(true),
+        NONE((Void) null),
         IGNITE_CREEPER("creeper"),
         EDIT,
         LEAVE_BOAT("exit.boat"),
@@ -271,7 +274,7 @@ public class InteractiveTagManager {
         @Getter
         private final String value;
 
-        InteractiveTag(boolean isNone) {
+        InteractiveTag(Void isNone) {
             this.value = "";
         }