Skip to content

Instantly share code, notes, and snippets.

@IllusionTheDev
Created April 2, 2022 23:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IllusionTheDev/8b0761be3b699fcfc0c082b753e6f063 to your computer and use it in GitHub Desktop.
Save IllusionTheDev/8b0761be3b699fcfc0c082b753e6f063 to your computer and use it in GitHub Desktop.
Easy client-side entity metadata
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);
}
}
}
@IllusionTheDev
Copy link
Author

UPDATE: As of 1.19, the metadata packet system changed slightly. This class is no longer necessary, instead just create a list of watchable objects.

image

@connection-lost
Copy link

@IllusionTheDev Thanks for the update on packet format. I've got 2 thoughts...

  1. How did you figured out the packet format has changed? Is there some changelog published somewhere?
  2. Consider update your post on Spigot regarding this new format.

@IllusionTheDev
Copy link
Author

@IllusionTheDev Thanks for the update on packet format. I've got 2 thoughts...

  1. How did you figured out the packet format has changed? Is there some changelog published somewhere?
  2. Consider update your post on Spigot regarding this new format.

Hey there, thanks for bringing this to my attention.

1 - I originally figured this out because someone on the SpigotMC discord server was having issues with this packet (I usually help out over there), and I was able to replicate the issue with the code on my gist. Finding the solution was a bit tougher, but eventually I found it by looking at old issues in the ProtocolLib github page (it was some obscure issue from December 2022)

2 - Will do

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