Added NbtDataImpl to support writing custom item NBT data to the client

This commit is contained in:
themode 2020-11-07 19:39:22 +01:00
parent 7bdfc93334
commit 224626bdbd
11 changed files with 252 additions and 54 deletions

View File

@ -1,3 +1,3 @@
asmVersion=8.0.1
mixinVersion=0.8
hephaistos_version=v1.1.4
hephaistos_version=v1.1.5

View File

@ -56,11 +56,25 @@ public interface Data {
*
* @param key the key
* @param value the value object, null to remove the key
* @param type the value type, can be null if not in a {@link SerializableData}
* @param type the value type, {@link #set(String, Object)} can be used instead.
* null if {@code value} is also null
* @param <T> the value generic
*/
<T> void set(@NotNull String key, @Nullable T value, @Nullable Class<T> type);
/**
* Assigns a value to a specific key.
* <p>
* Will by default call {@link #set(String, Object, Class)} with the type sets to {@link T#getClass()}.
*
* @param key the key
* @param value the value object, null to remove the key
* @param <T> the value generic
*/
default <T> void set(@NotNull String key, @Nullable T value) {
set(key, value, value != null ? (Class<T>) value.getClass() : null);
}
/**
* Retrieves a value based on its key.
*

View File

@ -26,7 +26,7 @@ public interface DataContainer {
* Default implementations are {@link DataImpl} and {@link SerializableDataImpl} depending
* on your use-case.
*
* @param data the {@link Data} of this container, null to remove it
* @param data the new {@link Data} of this container, null to remove it
*/
void setData(@Nullable Data data);

View File

@ -14,12 +14,20 @@ public class DataImpl implements Data {
protected final ConcurrentHashMap<String, Object> data = new ConcurrentHashMap<>();
/**
* Data key -> Class
* Used to know the type of an element of this data object (for serialization purpose)
*/
protected final ConcurrentHashMap<String, Class> dataType = new ConcurrentHashMap<>();
@Override
public <T> void set(@NotNull String key, @Nullable T value, @Nullable Class<T> type) {
if (value != null) {
this.data.put(key, value);
this.dataType.put(key, type);
} else {
this.data.remove(key);
this.dataType.remove(key);
}
}
@ -54,6 +62,7 @@ public class DataImpl implements Data {
public Data copy() {
DataImpl data = new DataImpl();
data.data.putAll(this.data);
data.dataType.putAll(this.dataType);
return data;
}

View File

@ -86,8 +86,7 @@ public final class DataManager {
*
* @param clazz the data class
* @param <T> the data type
* @return the {@link DataType} associated to the class
* @throws NullPointerException if none is found
* @return the {@link DataType} associated to the class, null if not found
*/
@Nullable
public <T> DataType<T> getDataType(@NotNull Class<T> clazz) {

View File

@ -0,0 +1,52 @@
package net.minestom.server.data;
import net.minestom.server.utils.NBTUtils;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import java.util.Map;
/**
* A data implementation backed by a {@link org.jglrxavpok.hephaistos.nbt.NBTCompound}.
*/
public class NbtDataImpl extends DataImpl {
// Used to know if a nbt key is from a Data object, should NOT be changed
public static final String KEY_PREFIX = "nbtdata_";
@NotNull
@Override
public Data copy() {
DataImpl data = new NbtDataImpl();
data.data.putAll(this.data);
data.dataType.putAll(this.dataType);
return data;
}
/**
* Writes all the data into a {@link NBTCompound}.
*
* @param nbtCompound the nbt compound to write to
* @throws NullPointerException if the type of a data is not a primitive nbt type and therefore not supported
* (you can use {@link DataType#encode(BinaryWriter, Object)} to use byte array instead)
*/
public void writeToNbt(@NotNull NBTCompound nbtCompound) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
final String key = entry.getKey();
final Object value = entry.getValue();
final Class type = dataType.get(key);
final NBT nbt = NBTUtils.toNBT(value, type, false);
Check.notNull(nbt,
"The type '" + type + "' is not supported within NbtDataImpl, if you wish to use a custom type you can encode the value into a byte array using a DataType");
nbtCompound.set(KEY_PREFIX + key, nbt);
}
}
}

View File

@ -14,7 +14,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* {@link SerializableData} implementation based on {@link DataImpl}
* {@link SerializableData} implementation based on {@link DataImpl}.
*/
public class SerializableDataImpl extends DataImpl implements SerializableData {
@ -25,15 +25,9 @@ public class SerializableDataImpl extends DataImpl implements SerializableData {
private static final ConcurrentHashMap<String, Class> nameToClassMap = new ConcurrentHashMap<>();
/**
* Data key -> Class
* Used to know the type of an element of this data object (for serialization purpose)
*/
private final ConcurrentHashMap<String, Class> dataType = new ConcurrentHashMap<>();
/**
* Set a value to a specific key
* Sets a value to a specific key.
* <p>
* WARNING: the type needs to be registered in {@link DataManager}
* WARNING: the type needs to be registered in {@link DataManager}.
*
* @param key the key
* @param value the value object
@ -42,18 +36,12 @@ public class SerializableDataImpl extends DataImpl implements SerializableData {
* @throws UnsupportedOperationException if {@code type} is not registered in {@link DataManager}
*/
@Override
public <T> void set(@NotNull String key, @Nullable T value, @NotNull Class<T> type) {
if (value != null) {
if (DATA_MANAGER.getDataType(type) == null) {
throw new UnsupportedOperationException("Type " + type.getName() + " hasn't been registered in DataManager#registerType");
}
this.data.put(key, value);
this.dataType.put(key, type);
} else {
this.data.remove(key);
this.dataType.remove(key);
public <T> void set(@NotNull String key, @Nullable T value, @Nullable Class<T> type) {
if (type != null && DATA_MANAGER.getDataType(type) == null) {
throw new UnsupportedOperationException("Type " + type.getName() + " hasn't been registered in DataManager#registerType");
}
super.set(key, value, type);
}
@NotNull

View File

@ -3,6 +3,7 @@ package net.minestom.server.item;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.data.Data;
import net.minestom.server.data.DataContainer;
import net.minestom.server.data.NbtDataImpl;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.inventory.Inventory;
@ -525,7 +526,8 @@ public class ItemStack implements DataContainer {
!attributes.isEmpty() ||
hideFlag != 0 ||
customModelData != 0 ||
(itemMeta != null && itemMeta.hasNbt());
(itemMeta != null && itemMeta.hasNbt()) ||
(data instanceof NbtDataImpl && !data.isEmpty());
}
/**
@ -557,16 +559,25 @@ public class ItemStack implements DataContainer {
final Data data = getData();
if (data != null)
itemStack.setData(data.copy());
return itemStack;
}
@Nullable
@Override
public Data getData() {
return data;
}
/**
* Sets the data of this item.
* <p>
* It is recommended to use {@link NbtDataImpl} if you want the data to be passed to the client.
*
* @param data the new {@link Data} of this container, null to remove it
*/
@Override
public void setData(Data data) {
public void setData(@Nullable Data data) {
this.data = data;
}
@ -676,6 +687,13 @@ public class ItemStack implements DataContainer {
return null;
}
/**
* Creates a {@link NBTCompound} containing the data of this item.
* <p>
* WARNING: modifying the returned nbt will not affect the item.
*
* @return this item nbt
*/
@NotNull
public NBTCompound toNBT() {
NBTCompound compound = new NBTCompound()

View File

@ -1,9 +1,13 @@
package net.minestom.server.utils;
import net.minestom.server.MinecraftServer;
import net.minestom.server.attribute.Attribute;
import net.minestom.server.attribute.AttributeOperation;
import net.minestom.server.chat.ChatParser;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.data.Data;
import net.minestom.server.data.DataType;
import net.minestom.server.data.NbtDataImpl;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.item.Enchantment;
import net.minestom.server.item.ItemStack;
@ -15,6 +19,9 @@ import net.minestom.server.item.metadata.ItemMeta;
import net.minestom.server.registry.Registries;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -40,7 +47,7 @@ public final class NBTUtils {
* @param items the items to save
* @param destination the inventory destination
*/
public static void loadAllItems(NBTList<NBTCompound> items, Inventory destination) {
public static void loadAllItems(@NotNull NBTList<NBTCompound> items, @NotNull Inventory destination) {
destination.clear();
for (NBTCompound tag : items) {
Material item = Registries.getMaterial(tag.getString("id"));
@ -55,7 +62,7 @@ public final class NBTUtils {
}
}
public static void saveAllItems(NBTList<NBTCompound> list, Inventory inventory) {
public static void saveAllItems(@NotNull NBTList<NBTCompound> list, @NotNull Inventory inventory) {
for (int i = 0; i < inventory.getSize(); i++) {
final ItemStack stack = inventory.getItemStack(i);
NBTCompound nbt = new NBTCompound();
@ -72,7 +79,8 @@ public final class NBTUtils {
}
}
public static void writeEnchant(NBTCompound nbt, String listName, Map<Enchantment, Short> enchantmentMap) {
public static void writeEnchant(@NotNull NBTCompound nbt, @NotNull String listName,
@NotNull Map<Enchantment, Short> enchantmentMap) {
NBTList<NBTCompound> enchantList = new NBTList<>(NBTTypes.TAG_Compound);
for (Map.Entry<Enchantment, Short> entry : enchantmentMap.entrySet()) {
final Enchantment enchantment = entry.getKey();
@ -86,7 +94,8 @@ public final class NBTUtils {
nbt.set(listName, enchantList);
}
public static ItemStack readItemStack(BinaryReader reader) {
@NotNull
public static ItemStack readItemStack(@NotNull BinaryReader reader) {
final boolean present = reader.readBoolean();
if (!present) {
@ -116,7 +125,7 @@ public final class NBTUtils {
return item;
}
public static void loadDataIntoItem(ItemStack item, NBTCompound nbt) {
public static void loadDataIntoItem(@NotNull ItemStack item, @NotNull NBTCompound nbt) {
if (nbt.containsKey("Damage")) item.setDamage(nbt.getInt("Damage"));
if (nbt.containsKey("Unbreakable")) item.setUnbreakable(nbt.getInt("Unbreakable") == 1);
if (nbt.containsKey("HideFlags")) item.setHideFlag(nbt.getInt("HideFlags"));
@ -182,6 +191,21 @@ public final class NBTUtils {
if (itemMeta == null)
return;
itemMeta.read(nbt);
NbtDataImpl customData = null;
for (String key : nbt.getKeys()) {
if (key.startsWith(NbtDataImpl.KEY_PREFIX)) {
if (customData == null) {
customData = new NbtDataImpl();
}
final NBT keyNbt = nbt.get(key);
final String dataKey = key.replaceFirst(NbtDataImpl.KEY_PREFIX, "");
final Object dataValue = fromNBT(keyNbt);
customData.set(dataKey, dataValue);
}
}
}
public static void loadEnchantments(NBTList<NBTCompound> enchantments, EnchantmentSetter setter) {
@ -226,7 +250,7 @@ public final class NBTUtils {
}
}
public static void saveDataIntoNBT(ItemStack itemStack, NBTCompound itemNBT) {
public static void saveDataIntoNBT(@NotNull ItemStack itemStack, @NotNull NBTCompound itemNBT) {
// Unbreakable
if (itemStack.isUnbreakable()) {
itemNBT.setInt("Unbreakable", 1);
@ -317,11 +341,86 @@ public final class NBTUtils {
// End custom model data
// Start custom meta
final ItemMeta itemMeta = itemStack.getItemMeta();
if (itemMeta != null) {
itemMeta.write(itemNBT);
{
final ItemMeta itemMeta = itemStack.getItemMeta();
if (itemMeta != null) {
itemMeta.write(itemNBT);
}
}
// End custom meta
// Start NbtData data
{
final Data data = itemStack.getData();
if (data instanceof NbtDataImpl) {
NbtDataImpl nbtData = (NbtDataImpl) data;
nbtData.writeToNbt(itemNBT);
}
}
// End NbtData
}
@Nullable
public static NBT toNBT(@NotNull Object value, @NotNull Class type, boolean supportDataType) {
type = PrimitiveConversion.getObjectClass(type);
if (type.equals(Boolean.class)) {
// No boolean type in NBT
return new NBTByte((byte) (((boolean) value) ? 1 : 0));
} else if (type.equals(Byte.class)) {
return new NBTByte((byte) value);
} else if (type.equals(Character.class)) {
// No char type in NBT
return new NBTShort((short) value);
} else if (type.equals(Short.class)) {
return new NBTShort((short) value);
} else if (type.equals(Integer.class)) {
return new NBTInt((int) value);
} else if (type.equals(Long.class)) {
return new NBTLong((long) value);
} else if (type.equals(Float.class)) {
return new NBTFloat((float) value);
} else if (type.equals(Double.class)) {
return new NBTDouble((double) value);
} else if (type.equals(String.class)) {
return new NBTString((String) value);
} else if (type.equals(Byte[].class)) {
return new NBTByteArray((byte[]) value);
} else if (type.equals(Integer[].class)) {
return new NBTIntArray((int[]) value);
} else if (type.equals(Long[].class)) {
return new NBTLongArray((long[]) value);
} else {
if (supportDataType) {
// Custom NBT type, try to encode using the data manager
DataType dataType = MinecraftServer.getDataManager().getDataType(type);
Check.notNull(dataType, "The type '" + type + "' is not registered in DataManager and not a primitive type.");
BinaryWriter writer = new BinaryWriter();
dataType.encode(writer, value);
final byte[] encodedValue = writer.toByteArray();
return new NBTByteArray(encodedValue);
} else {
return null;
}
}
}
public static Object fromNBT(@NotNull NBT nbt) {
if (nbt instanceof NBTNumber) {
return ((NBTNumber) nbt).getValue();
} else if (nbt instanceof NBTString) {
return ((NBTString) nbt).getValue();
} else if (nbt instanceof NBTByteArray) {
return ((NBTByteArray) nbt).getValue();
} else if (nbt instanceof NBTIntArray) {
return ((NBTIntArray) nbt).getValue();
} else if (nbt instanceof NBTLongArray) {
return ((NBTLongArray) nbt).getValue();
}
throw new UnsupportedOperationException("NBT type " + nbt.getClass() + " is not handled properly.");
}
@FunctionalInterface

View File

@ -1,25 +1,44 @@
package net.minestom.server.utils;
import java.util.HashMap;
import java.util.Map;
public class PrimitiveConversion {
private static Map<Class, Class> primitiveToBoxedTypeMap = new HashMap<>();
static {
// Primitive
primitiveToBoxedTypeMap.put(boolean.class, Boolean.class);
primitiveToBoxedTypeMap.put(byte.class, Byte.class);
primitiveToBoxedTypeMap.put(char.class, Character.class);
primitiveToBoxedTypeMap.put(short.class, Short.class);
primitiveToBoxedTypeMap.put(int.class, Integer.class);
primitiveToBoxedTypeMap.put(long.class, Long.class);
primitiveToBoxedTypeMap.put(float.class, Float.class);
primitiveToBoxedTypeMap.put(double.class, Double.class);
// Primitive one dimension array
primitiveToBoxedTypeMap.put(boolean[].class, Boolean[].class);
primitiveToBoxedTypeMap.put(byte[].class, Byte[].class);
primitiveToBoxedTypeMap.put(char[].class, Character[].class);
primitiveToBoxedTypeMap.put(short[].class, Short[].class);
primitiveToBoxedTypeMap.put(int[].class, Integer[].class);
primitiveToBoxedTypeMap.put(long[].class, Long[].class);
primitiveToBoxedTypeMap.put(float[].class, Float[].class);
primitiveToBoxedTypeMap.put(double[].class, Double[].class);
}
/**
* Converts primitive types to their boxed version.
* <p>
* Used to avoid needing to double-check everything
*
* @param clazz the class to convert
* @return the boxed class type of the primitive one, {@code clazz} otherwise
*/
public static Class getObjectClass(Class clazz) {
if (clazz == boolean.class)
return Boolean.class;
if (clazz == byte.class)
return Byte.class;
if (clazz == char.class)
return Character.class;
if (clazz == short.class)
return Short.class;
if (clazz == int.class)
return Integer.class;
if (clazz == long.class)
return Long.class;
if (clazz == float.class)
return Float.class;
if (clazz == double.class)
return Double.class;
return clazz;
return primitiveToBoxedTypeMap.getOrDefault(clazz, clazz);
}
public static String getObjectClassString(String clazz) {

View File

@ -7,7 +7,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/**
* Convenient class to check for common exceptions types.
* Convenient class to check for common exceptions.
*/
public final class Check {