diff --git a/plugin/src/main/java/eu/decentsoftware/holograms/api/utils/items/HologramItem.java b/plugin/src/main/java/eu/decentsoftware/holograms/api/utils/items/HologramItem.java
index a9723555..3d31cec6 100644
--- a/plugin/src/main/java/eu/decentsoftware/holograms/api/utils/items/HologramItem.java
+++ b/plugin/src/main/java/eu/decentsoftware/holograms/api/utils/items/HologramItem.java
@@ -86,13 +86,14 @@ private String parseExtras(Player player) {
@SuppressWarnings("deprecation")
private ItemStack applyNBT(Player player, ItemStack itemStack){
+ String parsedNbt = player != null ? PAPI.setPlaceholders(player, nbt) : nbt;
if (Version.afterOrEqual(Version.v1_20_R4)) {
- return NbtApiHook.applyNbtDataToItemStack(itemStack, nbt, player);
+ return NbtApiHook.applyNbtDataToItemStack(itemStack, NbtApiHook.ItemNbtData.fromJson(parsedNbt));
} else {
try {
- Bukkit.getUnsafe().modifyItemStack(itemStack, nbt);
+ Bukkit.getUnsafe().modifyItemStack(itemStack, parsedNbt);
} catch (Exception ex) {
- Log.warn("Failed to apply NBT Data to Item: %s", ex, nbt);
+ Log.warn("Failed to apply NBT Data to Item: %s", ex, parsedNbt);
}
return itemStack;
@@ -171,6 +172,7 @@ private String findExtras(String string) {
*
Enchantments (Will add {@value ENCHANTED_INDICATOR})
* Skull Owner/Texture (Texture is prioritized)
* CustomModelData (custom_model_data on newer MC versions).
+ * item_model
*
*
* @param itemStack The Item to convert into a HologramItem.
@@ -203,10 +205,8 @@ public static HologramItem fromItemStack(ItemStack itemStack) {
}
}
- float customModelData = NbtApiHook.extractCustomModelData(itemStack);
- if (customModelData > 0.0) {
- stringBuilder.append("{CustomModelData:").append(customModelData).append('}');
- }
+ NbtApiHook.ItemNbtData nbtRead = NbtApiHook.readData(itemStack);
+ stringBuilder.append(nbtRead.getJson());
return new HologramItem(stringBuilder.toString());
}
diff --git a/plugin/src/main/java/eu/decentsoftware/holograms/hook/NbtApiHook.java b/plugin/src/main/java/eu/decentsoftware/holograms/hook/NbtApiHook.java
index 137dd1c5..d5740dc9 100644
--- a/plugin/src/main/java/eu/decentsoftware/holograms/hook/NbtApiHook.java
+++ b/plugin/src/main/java/eu/decentsoftware/holograms/hook/NbtApiHook.java
@@ -1,15 +1,22 @@
package eu.decentsoftware.holograms.hook;
+import com.google.common.base.Preconditions;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBTList;
-import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
import eu.decentsoftware.holograms.api.utils.Log;
-import eu.decentsoftware.holograms.api.utils.PAPI;
+import eu.decentsoftware.holograms.api.utils.reflect.ReflectMethod;
import eu.decentsoftware.holograms.api.utils.reflect.Version;
+import lombok.Data;
import lombok.experimental.UtilityClass;
-import org.bukkit.entity.Player;
+import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
/**
* This class provides a utility wrapper for the NBT-API library, allowing safe access to NBT data manipulation
@@ -25,6 +32,8 @@
public class NbtApiHook {
private static boolean loadedSuccessfully;
+ private static ReflectMethod setCustomModelData;
+ private static ReflectMethod setItemModel;
public static void initialize() {
loadedSuccessfully = NBT.preloadApi();
@@ -33,45 +42,68 @@ public static void initialize() {
} else {
Log.info("NBT-API loaded successfully.");
}
+
+ // Set up bukkit API access
+ if (Version.afterOrEqual(Version.v1_20_R4)) {
+ setCustomModelData = new ReflectMethod(ItemMeta.class, "setCustomModelData", Integer.class);
+ setItemModel = new ReflectMethod(ItemMeta.class, "setItemModel", NamespacedKey.class);
+ }
}
- public static ItemStack applyNbtDataToItemStack(ItemStack itemStack, String nbt, Player player) {
+ // Applies NBT data to an item for versions 1.20.5+
+ public static ItemStack applyNbtDataToItemStack(final ItemStack itemStack, ItemNbtData nbt) {
if (!loadedSuccessfully) {
return itemStack;
}
try {
- ReadWriteNBT originalNBT = NBT.itemStackToNBT(itemStack); // Used later for merge.
- ReadWriteNBT modifiableNBT = NBT.itemStackToNBT(itemStack);
- modifiableNBT.getOrCreateCompound("tag")
- .mergeCompound(NBT.parseNBT(player == null ? nbt : PAPI.setPlaceholders(player, nbt)));
- /*
- * DataFixerUtil has an issue where it expects to find "Count", due to expecting pre-1.20.5 NBT data,
- * but since we used a 1.20.5+ ItemStack to create the NBT is there only "count", which causes
- * DataFixerUtil to not find a valid NBT and does nothing.
- * This addition fixes that issue.
- */
- modifiableNBT.setByte("Count", (byte) 1);
- modifiableNBT = DataFixerUtil.fixUpItemData(modifiableNBT, DataFixerUtil.VERSION1_20_4, DataFixerUtil.getCurrentVersion());
- /*
- * Updating the NBT removes the modern NBT variants of enchants and alike, as Datafixer discards them.
- * So we have to manually merge them in again... Not pretty, but it does the job.
- */
- modifiableNBT.mergeCompound(originalNBT);
-
- return NBT.itemStackFromNBT(modifiableNBT);
+ ItemStack toModify = itemStack.clone();
+ // item_model was present in 1.21.2+
+ ItemMeta meta = toModify.getItemMeta();
+ if (nbt.getItemModel() != null && Version.afterOrEqual(Version.v1_21_R2)) {
+ setItemModel.invoke(meta, namespacedKeyFromString(nbt.getItemModel(), null));
+ }
+ if (nbt.getCustomModelData() != 0f) {
+ setCustomModelData.invoke(meta, ((Float) nbt.getCustomModelData()).intValue());
+ }
+ toModify.setItemMeta(meta);
+
+ return toModify;
} catch (Exception ex) {
Log.warn("Failed to apply NBT Data to Item: %s", ex, nbt);
return itemStack;
}
}
- public static float extractCustomModelData(ItemStack itemStack) {
+ public static ItemNbtData readData(ItemStack itemStack) {
+ if (!loadedSuccessfully) {
+ return ItemNbtData.EMPTY;
+ }
+
+ ReadWriteNBT nbt = NBT.itemStackToNBT(itemStack);
+ return new ItemNbtData(extractItemModel(nbt), extractCustomModelData(nbt));
+ }
+
+ private static String extractItemModel(ReadWriteNBT nbtItem) {
+ if (!loadedSuccessfully) {
+ return null;
+ }
+
+ String itemModel;
+ if (Version.afterOrEqual(Version.v1_21_R2)) {
+ itemModel = nbtItem.getOrCreateCompound("components")
+ .getString("minecraft:item_model");
+ } else {
+ itemModel = null;
+ }
+ return itemModel;
+ }
+
+ private static float extractCustomModelData(ReadWriteNBT nbtItem) {
if (!loadedSuccessfully) {
return 0f;
}
- ReadWriteNBT nbtItem = NBT.itemStackToNBT(itemStack);
float customModelData;
if (Version.afterOrEqual(Version.v1_21_R3)) {
// New structure components:{custom_model_data={floats[...]}} since 1.21.4
@@ -91,4 +123,126 @@ public static float extractCustomModelData(ItemStack itemStack) {
}
return customModelData;
}
+
+ // Taken from Bukkit 1.21 for backwards compatibility.
+ @Nullable
+ private static NamespacedKey namespacedKeyFromString(@NotNull String string, @Nullable Plugin defaultNamespace) {
+ // Paper - Return null for empty string, check length
+ Preconditions.checkArgument(string != null, "Input string must not be null");
+ if (string.isEmpty() || string.length() > Short.MAX_VALUE) return null;
+ // Paper end - Return null for empty string, check length
+
+ String[] components = string.split(":", 3);
+ if (components.length > 2) {
+ return null;
+ }
+
+ String key = (components.length == 2) ? components[1] : "";
+ if (components.length == 1) {
+ String value = components[0];
+ if (value.isEmpty() || !isValidKey(value)) {
+ return null;
+ }
+
+ return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, value) : NamespacedKey.minecraft(value);
+ } else if (components.length == 2 && !isValidKey(key)) {
+ return null;
+ }
+
+ String namespace = components[0];
+ if (namespace.isEmpty()) {
+ return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, key) : NamespacedKey.minecraft(key);
+ }
+
+ if (!isValidNamespace(namespace)) {
+ return null;
+ }
+
+ return new NamespacedKey(namespace, key);
+ }
+
+ private static boolean isValidNamespaceChar(char c) {
+ return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
+ }
+
+ private static boolean isValidKeyChar(char c) {
+ return isValidNamespaceChar(c) || c == '/';
+ }
+
+ private static boolean isValidNamespace(String namespace) {
+ int len = namespace.length();
+ if (len == 0) {
+ return false;
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (!isValidNamespaceChar(namespace.charAt(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean isValidKey(String key) {
+ int len = key.length();
+ if (len == 0) {
+ return false;
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (!isValidKeyChar(key.charAt(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Represents the result of reading data from an item stack.
+ * This is used to prevent redundancy with NBTAPI.
+ */
+ @Data
+ public static class ItemNbtData {
+ public static final ItemNbtData EMPTY = new ItemNbtData(null, 0f);
+
+ private final String itemModel;
+ private final float customModelData;
+ private String json;
+
+ public ItemNbtData(String itemModel, float customModelData) {
+ this.itemModel = itemModel;
+ this.customModelData = customModelData;
+ this.json = toJson();
+ }
+
+ /**
+ * Converts this result to json.
+ *
+ * @return the json.
+ */
+ private String toJson() {
+ if ((this.itemModel == null || this.itemModel.isEmpty()) && this.customModelData == 0f)
+ return "";
+
+ // Use Gson to allow for easier expansion in the future.
+ JsonObject object = new JsonObject();
+ if (this.itemModel != null && !this.itemModel.isEmpty())
+ object.addProperty("minecraft:item_model", this.itemModel);
+ if (this.customModelData != 0f)
+ object.addProperty("CustomModelData", this.customModelData);
+ return object.toString();
+ }
+
+ public static ItemNbtData fromJson(String json) {
+ if (json == null || json.isEmpty())
+ return EMPTY;
+
+ JsonObject object = new JsonParser().parse(json).getAsJsonObject();
+ String itemModel = object.has("minecraft:item_model") ? object.get("minecraft:item_model").getAsString() : null;
+ float customModelData = object.has("CustomModelData") ? object.get("CustomModelData").getAsFloat() : 0f;
+ return new ItemNbtData(itemModel, customModelData);
+ }
+ }
}