Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -171,6 +172,7 @@ private String findExtras(String string) {
* <li>Enchantments (Will add {@value ENCHANTED_INDICATOR})</li>
* <li>Skull Owner/Texture (Texture is prioritized)</li>
* <li>CustomModelData (custom_model_data on newer MC versions).</li>
* <li>item_model</li>
* </ul>
*
* @param itemStack The Item to convert into a HologramItem.
Expand Down Expand Up @@ -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());
}

Expand Down
204 changes: 179 additions & 25 deletions plugin/src/main/java/eu/decentsoftware/holograms/hook/NbtApiHook.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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);
}
}
}