Advanced PacketEvents Example: Combining our Knowledge
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 org.jspecify.annotations.NullMarked;import org.jspecify.annotations.Nullable;
import java.util.Map;import java.util.UUID;import java.util.concurrent.ConcurrentHashMap;
@NullMarkedpublic class ExampleListener implements PacketListener {
private @Nullable 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 (this.fakeArmorStand == null) { // Generate a random UUID UUID uuid = UUID.randomUUID(); // Generate an Entity ID int entityId = SpigotReflectionUtil.generateEntityId();
this.fakeArmorStand = new FakeArmorStand(uuid, entityId); }
// Spawn the Armor Stand at the user's current location Location spawnLocation = SpigotConversionUtil.fromBukkitLocation(player.getLocation()); this.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 (this.fakeArmorStand != null && entityId == this.fakeArmorStand.entityId) { // Increment their clicks int clicks = this.fakeArmorStand.clicks.getOrDefault(user.getUUID(), 0) + 1; this.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( this.entityId, this.uuid, EntityTypes.ARMOR_STAND, location, location.getYaw(), // Head yaw 0, // No additional data null // We won't specify any initial velocity ); user.sendPacket(packet); } }}