Skip to content

Entity lifecycle

Entity spawning

When an entity is added to the world, the server dispatches a ClientboundAddEntityPacket. While this packet signals the creation of an entity, it omits key metadata - notably, the custom name for armor stands is absent from its payload. As a result, this packet alone is insufficient for identifying or tracking named entities.

However, the packet does provide essential baseline data, such as the entity's UUID, ID, type, position, movement, and rotation, which can be useful for entity identification.

Entity metadata

Shortly after the entity is spawned, the server dispatches a ClientboundSetEntityDataPacket. This packet contains the metadata for the entity that was spawned. This packet is sent several times thoroughout the entity's lifetime with the updated values.

This metadata includes the custom name, health, and many other properties that can be used to identify and track entities. This packet is particularly useful for identifying entities because it contains the custom name for armor stands, which is not present in the ClientboundAddEntityPacket.

The ClientboundSetEntityDataPacket contains a list of SynchedEntityData.DataValue values.

Extracting data from ClientboundSetEntityDataPacket

Mixin targeting ClientPacketListener
java
@Mixin(ClientPacketListener.class)
public class ClientPacketListenerMixin {
    @Inject(method = "handleSetEntityData", at = @At("TAIL"))
    private void onHandleSetEntityData(ClientboundSetEntityDataPacket packet, CallbackInfo ci, @Local Entity entity) {
        if (entity == null) return;
    }
}
Unpacking the packet manually
kotlin
object Example {
    init {
        on<PacketEvent.Receive, ClientboundSetEntityDataPacket> {
            // Refer to the custom EventBus implementation discussed earlier if you don't understand where the packedItems() is coming from.
            packedItems().forEach { data: SynchedEntityData.DataValue<*> ->
                // Do something with the data.
                // Name usually has the entry id 2 and is an Optional<Component>
            }
        }
    }
}

Entity destruction

When an entity needs to be removed from the client's view or memory, the server dispatches a ClientboundRemoveEntitiesPacket. Depending on your use case, there are three primary ways to detect and handle entity destruction:

  • Mixin into LivingEntity.die(): Specifically designed for detecting when a living entity dies.

  • Mixin into Entity.onRemoval(): A catch-all hook that runs whenever any entity is removed from the client world for any reason. It provides a RemovalReason.

  • Packet Listening: Ideal for general client-side cleanup as it provides the raw entity IDs that are being discarded.

Mixing into LivingEntity
java
@Mixin(LivingEntity.class)
public class LivingEntityMixin {
    @Inject(method = "die", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;setPose(Lnet/minecraft/world/entity/Pose;)V"))
    private void onDie(DamageSource damageSource, CallbackInfo ci) {
        final LivingEntity entity = self();
        // Do something with the entity
    }

    @Unique
    private LivingEntity self() {
        return (LivingEntity) (Object) this;
    }
}
Mixing into Entity
java
@Mixin(Entity.class)
public abstract class EntityMixin {
    @Inject(method = "onRemoval", at = @At("RETURN"))
    private void onRemove(Entity.RemovalReason reason, CallbackInfo ci) {
        final Entity entity = self();
        // Do something with the entity
    }

    @Unique
    private Entity self() {
        return (Entity) (Object) this;
    }
}
Mixing into ClientPacketListener
java
@Mixin(ClientPacketListener.class)
public class ClientPacketListenerMixin {
    @Inject(method = "handleRemoveEntities", at = @At("TAIL"))
    private void onHandleRemoveEntities(ClientboundRemoveEntitiesPacket packet, CallbackInfo ci) {
        final IntList entityIds = packet.getEntityIds();
        // Do something with the entity IDs
    }
}
Using the packet
kotlin
object Example {
    init {
        on<PacketEvent.Receive, ClientboundRemoveEntitiesPacket> {
            val level = client.level ?: return@on
            // There's a list of entity IDs accessible through the method getEntityIds()
            // getEntityIds() -> entityIds in kotlin
            entityIds.forEach { id: Int ->
                val entity = level.getEntity(id) ?: return@forEach
                // Do something with the entity
            }
        }
    }
}

Not affiliated with Mojang, Microsoft, or Hypixel.