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);
}
}
}