Last active
June 1, 2023 08:50
-
-
Save IllusionTheDev/883eb3aa8b7c6d4b849f9a9eed145dc1 to your computer and use it in GitHub Desktop.
Test client-side entity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
oh wow that seems pretty complicated
this is really cool