Skip to content

Instantly share code, notes, and snippets.

@IllusionTheDev
Last active June 1, 2023 08:50
Show Gist options
  • Save IllusionTheDev/883eb3aa8b7c6d4b849f9a9eed145dc1 to your computer and use it in GitHub Desktop.
Save IllusionTheDev/883eb3aa8b7c6d4b849f9a9eed145dc1 to your computer and use it in GitHub Desktop.
Test client-side entity
package me.illusion.test;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.WrappedBlockData;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
import com.comphenix.protocol.wrappers.WrappedWatchableObject;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Helps with metadata packet handling.
*
* @author Illusion
*/
public class EasyMetadataPacket {
// -- SECTION START --
// This section is responsible for boxing primitive values, to be serialized.
private static final Map<String, Class<?>> PRIMITIVES = new HashMap<>(); // Using String as 1st param because Class<?> has no hashcode
static {
PRIMITIVES.put("int", Integer.class);
PRIMITIVES.put("byte", Byte.class);
PRIMITIVES.put("boolean", Boolean.class);
}
// Lot of protocollib copy-pasted code due to field accessors
// -- PROTOCOLLIB START
private static final Class<?> HANDLE_TYPE = MinecraftReflection.getDataWatcherClass();
public EasyMetadataPacket(Object entity) {
this.entity = entity;
}
private static ConstructorAccessor constructor = null;
// -- SECTION END --
private final Object entity;
// -- Series of Maps <index, object>, correspondent to the data watcher
private final Map<Integer, Object> emptyOptionalData = new HashMap<>(); // Empty optionals, used for chatcomponents if the text is empty
private final Map<Integer, Object> optionalData = new HashMap<>(); // Optional data, used for data types that have the "Opt" prefix
private final Map<Integer, Object> data = new HashMap<>(); // All other data
private static Object newHandle(Object entity) {
if (constructor == null) {
constructor = Accessors.getConstructorAccessor(HANDLE_TYPE, MinecraftReflection.getEntityClass());
}
return constructor.invoke(entity);
}
// -- PROTOCOLLIB END
/**
* Debugs values
*/
public void print() {
for (Map.Entry<Integer, Object> entry : data.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
/**
* Writes an object into the internal data, to later be serialized into
* the DataWatcher
*
* @param index - The object index
* @param value - The object value
*/
public void write(int index, Object value) {
data.put(index, value);
}
/**
* Writes an Optional Object into the internal data, to later be serialized
* into the DataWatcher
*
* @param index - The object index
* @param value - The object value
*/
public void writeOptional(int index, Object value) {
optionalData.put(index, value);
}
/**
* Writes an empty optional object into the internal data, to later
* be serialized into the DataWatcher
*
* @param index - Object index
* @param randomValue - Random object instance, used to obtain the class
*/
public void writeEmptyData(int index, Object randomValue) {
emptyOptionalData.put(index, randomValue);
}
/**
* Exports the metadata as a List<WrappedWatchableObject>,
* to be used directly into the metadata packet.
*
* @return - Metadata values
*/
public List<WrappedWatchableObject> export() {
// Makes a data watcher, uses fake internal entity if no entity is provided.
WrappedDataWatcher watcher = (entity == null) ? new WrappedDataWatcher() : new WrappedDataWatcher(newHandle(entity));
writeData(watcher, emptyOptionalData, true, true); // Writes empty optional data
writeData(watcher, optionalData, true, false); // Writes optional data
writeData(watcher, data, false, false); // Writes remainding data
return watcher.getWatchableObjects();
}
/**
* Method to write internal data. Pure spaghetti
*
* @param watcher - Data watcher to write to
* @param data - Internal data to write
* @param optional - TRUE if data is purely optional, FALSE otherwise
* @param empty - TRUE if data is purely empty and optional, FALSE otherwise
*/
private void writeData(WrappedDataWatcher watcher, Map<Integer, Object> data, boolean optional, boolean empty) {
for (Map.Entry<Integer, Object> entry : data.entrySet()) { // Loops through all data
int index = entry.getKey();
Object value = entry.getValue();
Class<?> clazz = value.getClass(); // Obtains value class, to later be implemented as a serializer
if (clazz.isPrimitive()) // Boxes primitives
clazz = PRIMITIVES.get(clazz.getName());
if (clazz.equals(ItemStack.class)) { // Item serializer special handling
watcher.setObject(index, WrappedDataWatcher.Registry.getItemStackSerializer(false), value);
continue;
}
if (clazz.equals(WrappedChatComponent.class)) { // Chat serializer special handling
if (optional) {
value = empty ? Optional.empty() : Optional.of(((WrappedChatComponent) value).getHandle());
}
watcher.setObject(index, WrappedDataWatcher.Registry.getChatComponentSerializer(optional), value);
continue;
}
if(clazz.equals(WrappedBlockData.class)) {
if (optional) {
value = empty ? Optional.empty() : Optional.of(((WrappedBlockData) value).getHandle());
}
watcher.setObject(index, WrappedDataWatcher.Registry.getBlockDataSerializer(optional), value);
continue;
}
// Serializes everything else
watcher.setObject(index, WrappedDataWatcher.Registry.get(clazz, optional), value);
}
}
}
package me.illusion.test;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.BukkitUnwrapper;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID;
public class TestPlugin extends JavaPlugin {
private static int ENTITY_ID = 10000;
private final PacketType spawnType = PacketType.Play.Server.SPAWN_ENTITY_LIVING;
@Override
public void onEnable() {
registerPacketListeners();
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if(!(sender instanceof Player)) {
sender.sendMessage("There is a robot among us. Leave.");
return true;
}
Player player = (Player) sender;
spawnArmorStand(player, player.getLocation());
return true;
}
// -- DEBUG PACKET --
private void registerPacketListeners() {
ProtocolManager manager = ProtocolLibrary.getProtocolManager();
manager.addPacketListener(new PacketAdapter(this, PacketType.Play.Client.USE_ENTITY) {
@Override
public void onPacketReceiving(PacketEvent event) {
Player player = event.getPlayer();
PacketContainer packet = event.getPacket();
int entityId = packet.getIntegers().read(0);
player.sendMessage("Called USE_ENTITY (id = " + entityId + ")");
}
});
manager.addPacketListener(new PacketAdapter(this, spawnType) {
@Override
public void onPacketSending(PacketEvent event) {
Player player = event.getPlayer();
PacketContainer packet = event.getPacket();
int entityId = packet.getIntegers().read(0);
player.sendMessage("Called SPAWN_ENTITY (id = " + entityId + ")");
}
});
}
// -- SPAWN PACKET --
private void spawnArmorStand(Player player, Location location) {
PacketContainer packet = createSpawnPacket(ENTITY_ID, location);
PacketContainer packet2 = createMetadataPacket(ENTITY_ID++, "Test name");
try {
ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet);
ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet2);
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// -- PACKET UTILS --
/**
* Creates entity spawn packet
*
* @param entityId - The entity ID
* @param location - The spawn location
* @return SPAWN_ENTITY_LIVING packet, change to SPAWN_ENTITY if having issues
*/
private PacketContainer createSpawnPacket(int entityId, Location location) {
ProtocolManager manager = ProtocolLibrary.getProtocolManager();
PacketContainer spawn = manager.createPacket(spawnType);
spawn.getIntegers().writeSafely(0, entityId); // Entity ID
spawn.getUUIDs().writeSafely(0, UUID.randomUUID()); // Entity UUID
spawn.getIntegers().writeSafely(1, 1); // Entity type ID, 1 = Armor Stand
spawn.getDoubles().writeSafely(0, location.getX()); // Location X
spawn.getDoubles().writeSafely(1, location.getY()); // Location Y
spawn.getDoubles().writeSafely(2, location.getZ()); // Location Z
spawn.getBytes().writeSafely(0, (byte) (location.getYaw() / 256 * 360)); // Yaw, not used
spawn.getBytes().writeSafely(1, (byte) (location.getPitch() / 256 * 360)); // Pitch, not used
spawn.getBytes().writeSafely(2, (byte) (location.getPitch() / 256 * 360)); // Body rotation
spawn.getShorts().writeSafely(0, (short) 0); // Velocity X
spawn.getShorts().writeSafely(1, (short) 0); // Velocity Y
spawn.getShorts().writeSafely(2, (short) 0); // Velocity Z
return spawn;
}
/**
* Creates metadata packet, invisible by default
*
* @param name - The hologram text, non present if ""
* @return ENTITY_METADATA packet
*/
private PacketContainer createMetadataPacket(int entityId, String name) {
PacketContainer metadataPacket = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA); // Wrapped packet, for easy use
metadataPacket.getIntegers().write(0, entityId); // Assign internal entity ID
byte mask = 0x00; // Bitmask, armor-stand specific, NOT USED ON THE INDEX OF 1
mask = attach(mask, (byte) 0x01, true); // 0x01 = baby
// UNUSED:
// 0x04 = Has arms
// 0x08 = Has no baseplate
// 0x10 = Is marker
// Creates a metadata packet with NMS entity for data watcher ease of use. Pass NULL if NMS is giving issues
// The NMS entity is a weird hack I did after decompiling the wrapped data watcher, as I noticed it created a fake egg, and was having issues with metadata.
EasyMetadataPacket metadata = new EasyMetadataPacket(createInstance("EntityArmorStand"));
metadata.write(0, (byte) (0x00));
metadata.write(1, 0); // Air ticks
// Name
if (name.isEmpty())
metadata.writeEmptyData(2, WrappedChatComponent.fromText(""));
else
metadata.writeOptional(2, WrappedChatComponent.fromText(name));
metadata.write(3, !name.isEmpty()); // Name is visible
metadata.write(4, Boolean.TRUE); // Is silent
metadata.write(5, Boolean.TRUE); // No gravity
metadata.write(14, mask); // Armor stand properties
metadataPacket.getWatchableCollectionModifier().write(0, metadata.export()); // Exports and writes metadata into packet
return metadataPacket;
}
/**
* Adds a bit to a bitmask if a boolean is present
*
* @param defaultVal - The bitmask
* @param toAdd - The bit
* @param supposedToAdd - The boolean
* @return updated bitmask
*/
private byte attach(byte defaultVal, byte toAdd, boolean supposedToAdd) {
return supposedToAdd ? (byte) (defaultVal | toAdd) : defaultVal;
}
/**
* Creates an NMS entity instance using world,x,y,z constructor
*
* @param clazzName - The NMS entity class name, example: "EntityItem"
* @return NMS entity instance
*/
public Object createInstance(String clazzName) {
ConstructorAccessor constructor = Accessors.getConstructorAccessor(MinecraftReflection.getMinecraftClass(clazzName), MinecraftReflection.getNmsWorldClass(), Double.TYPE, Double.TYPE, Double.TYPE);
Object world = BukkitUnwrapper.getInstance().unwrapItem(Bukkit.getWorlds().get(0));
return constructor.invoke(world, 0, 0, 0);
}
}
@MolhamSYR
Copy link

oh wow that seems pretty complicated
this is really cool

@IllusionTheDev
Copy link
Author

UPDATE: As of 1.19, The metadata packet system changed slightly. The EasyMetadataPacket class is no longer required, here's a modern example of how to use the metadata packet:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment