Advanced PacketEvents Example: Combining our Knowledge

We're developing a more advanced project utilizing the PacketEvents API. We will combine everything we've learned so far.

In this example, we will develop a Minecraft Bukkit plugin that will spawn a client-sided Armor Stand, providing a click counter for each player.

First, we'll spawn an Armor Stand for the client whenever they join the server. Next, we'll wait for the client to send an "Interact" packet. We'll check if the client had interacted with our client-sided Armor Stand. If so, we'll increment the click counter for that player.

Previously, we mentioned that packets provide direct communication with the client, allowing us to give each user a unique experience. "Client-sided entity" in this context merely means that an entity is spawned for a particular client. Most importantly, the server is not informed of said entity, thus it will only be visible to the users you present it to.

import com.github.retrooper.packetevents.event.PacketListener;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.UserLoginEvent;
import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.User;
import com.github.retrooper.packetevents.protocol.world.Location;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity;
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
import io.github.retrooper.packetevents.util.SpigotReflectionUtil;
import org.bukkit.entity.Player;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class PacketEventsPacketListener implements PacketListener {
    private FakeArmorStand fakeArmorStand = null;

    @Override
    public void onUserLogin(UserLoginEvent event) {
        User user = event.getUser();
        Player player = event.getPlayer();

        // Create the Armor Stand (if we haven't already)
        if (fakeArmorStand == null) {
            // Generate a random UUID
            UUID uuid = UUID.randomUUID();
            // Generate an Entity ID
            int entityId = SpigotReflectionUtil.generateEntityId();

            fakeArmorStand = new FakeArmorStand(uuid, entityId);
        }
        
        // Spawn the Armor Stand at the user's current location
        Location spawnLocation = SpigotConversionUtil.fromBukkitLocation(player.getLocation());
        fakeArmorStand.spawn(user, spawnLocation);
    }

    @Override
    public void onPacketReceive(PacketReceiveEvent event) {
        User user = event.getUser();
        if (event.getPacketType() != PacketType.Play.Client.INTERACT_ENTITY) 
            return;
        // They interacted with an entity.
        WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
        // Retrieve that entity's ID
        int entityId = packet.getEntityId();

        // Check if the client interacted with the Armor Stand
        if (entityId == fakeArmorStand.entityId) {
            // Increment their clicks
            int clicks = fakeArmorStand.clicks.getOrDefault(user.getUUID(), 0) + 1;
            fakeArmorStand.clicks.put(user.getUUID(), clicks);
            user.sendMessage("You now have " + clicks + " clicks on the Armor Stand!");
        }
    }

    private static class FakeArmorStand {
        private final int entityId;
        private final UUID uuid;
        // Track their clicks
        private final Map<UUID, Integer> clicks = new ConcurrentHashMap<>();

        public FakeArmorStand(UUID uuid, int entityId) {
            this.uuid = uuid;
            this.entityId = entityId;
        }
        
        public void spawn(User user, Location location) {
            WrapperPlayServerSpawnEntity packet = new WrapperPlayServerSpawnEntity(
                    entityId,
                    uuid,
                    EntityTypes.ARMOR_STAND,
                    location,
                    location.getYaw(), // Head yaw
                    0, // No additional data
                    null // We won't specify any initial velocity
            );
            user.sendPacket(packet);
        }
    }
}

Last updated