mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-26 02:57:37 +01:00
Added NbtDataImpl to support writing custom item NBT data to the client
This commit is contained in:
parent
7bdfc93334
commit
224626bdbd
@ -1,3 +1,3 @@
|
||||
asmVersion=8.0.1
|
||||
mixinVersion=0.8
|
||||
hephaistos_version=v1.1.4
|
||||
hephaistos_version=v1.1.5
|
@ -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.
|
||||
*
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
52
src/main/java/net/minestom/server/data/NbtDataImpl.java
Normal file
52
src/main/java/net/minestom/server/data/NbtDataImpl.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user