/
NBT.java
456 lines (396 loc) · 16.8 KB
/
NBT.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
/*
* Copyright or © or Copr. QuartzLib contributors (2015 - 2020)
*
* This software is governed by the CeCILL-B license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL-B
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-B license and that you accept its terms.
*/
package fr.zcraft.quartzlib.components.nbt;
import fr.zcraft.quartzlib.tools.PluginLogger;
import fr.zcraft.quartzlib.tools.items.ItemUtils;
import fr.zcraft.quartzlib.tools.reflection.NMSException;
import fr.zcraft.quartzlib.tools.reflection.Reflection;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
/**
* This class provides various utilities to manipulate NBT data.
*/
public abstract class NBT {
static Class<?> MC_ITEM_STACK = null;
static Class<?> MC_NBT_TAG_COMPOUND = null;
/* ========== Item utilities ========== */
static Class<?> CB_CRAFT_ITEM_META = null;
private NBT() {
}
/**
* Returns the NBT tag for the specified item.
* The tag is read-write, and any modification applied to it will also be
* applied to the item's NBT tag.
*
* @param item The item to get the tag from.
* @return the NBT tag for the specified item.
* @throws NMSException If there was any issue while assigning NBT data.
*/
public static NBTCompound fromItemStack(ItemStack item) throws NMSException {
init();
try {
return new NBTCompound(getMcNBTCompound(item));
} catch (Exception ex) {
throw new NMSException("Unable to retrieve NBT data", ex);
}
}
/**
* Returns an NBT-like representation of the specified item meta.
* It is useful as a fallback if you need item data as an NBT format, but
* the actual NBT couldn't be retrieved for some reason.
*
* @param meta The item meta to get the data from.
* @return an NBT-like representation of the specified item meta.
*/
public static Map<String, Object> fromItemMeta(ItemMeta meta) {
Map<String, Object> itemData = new HashMap<>();
if (meta.hasDisplayName()) {
itemData.put("Name", meta.getDisplayName());
}
if (meta.hasLore()) {
itemData.put("Lore", meta.getLore());
}
return itemData;
}
/**
* Returns an NBT-like representation of the specified enchantments.
* It is useful as a fallback if you need item data as an NBT format, but
* the actual NBT couldn't be retrieved for some reason.
*
* @param enchants the enchantment list to get the data from.
* @return an NBT-like representation of the specified enchantments.
*/
public static List<Map<String, Object>> fromEnchantments(Map<Enchantment, Integer> enchants) {
List<Map<String, Object>> enchantList = new ArrayList<>();
for (Map.Entry<Enchantment, Integer> enchantment : enchants.entrySet()) {
final Map<String, Object> enchantmentData = new HashMap<>();
enchantmentData.put("key", enchantment.getKey());
enchantmentData.put("lvl", enchantment.getValue());
enchantList.add(enchantmentData);
}
return enchantList;
}
/* ========== Internal utilities ========== */
/**
* Returns an NBT-like representation of the item flags (HideFlags).
* It is useful as a fallback if you need item data as an NBT format, but
* the actual NBT couldn't be retrieved for some reason.
*
* @param itemFlags item flag set to get the data from.
* @return an NBT-like representation of the item flags (HideFlags).
*/
public static byte fromItemFlags(Set<ItemFlag> itemFlags) {
byte flags = 0;
for (ItemFlag flag : itemFlags) {
switch (flag) {
case HIDE_ENCHANTS:
flags += 1;
break;
case HIDE_ATTRIBUTES:
flags += 1 << 1;
break;
case HIDE_UNBREAKABLE:
flags += 1 << 2;
break;
case HIDE_DESTROYS:
flags += 1 << 3;
break;
case HIDE_PLACED_ON:
flags += 1 << 4;
break;
case HIDE_POTION_EFFECTS:
flags += 1 << 5;
break;
default:
break;
}
}
return flags;
}
/**
* Replaces the tags in the given ItemStack by the given tags.
* <p>This operation is only possible on a CraftItemStack. As a consequence,
* this method <strong>returns</strong> an {@link ItemStack}. If the given
* ItemStack was a CraftItemStack, the same instance will be returned, but
* in the other cases, it will be a new one (a copy).</p>
*
* @param item The ItemStack to change.
* @param tags The tags to place inside the stack.
* @return An item stack with the modification applied. It may (if you given
* a CraftItemStack) or may not (else) be the same instance as the given one.
* @throws NMSException if the operation cannot be executed.
* @see #addToItemStack(ItemStack, Map, boolean) This method is equivalent
* to this one with replace = true.
*/
public static ItemStack addToItemStack(ItemStack item, Map<String, Object> tags) throws NMSException {
return addToItemStack(item, tags, true);
}
/**
* Adds or replaces the tags in the given ItemStack by the given tags.
* <p>This operation is only possible on a CraftItemStack. As a consequence,
* this method <strong>returns</strong> an {@link ItemStack}. If the given
* ItemStack was a CraftItemStack, the same instance will be returned, but
* in the other cases, it will be a new one (a copy).</p>
*
* @param item The ItemStack to change.
* @param tags The tags to place inside the stack.
* @param replace {@code true} to replace the whole set of tags. If {@code
* false}, tags will be added.
* @return An item stack with the modification applied. It may (if you given
* a CraftItemStack) or may not (else) be the same instance as the given one.
* @throws NMSException if the operation cannot be executed.
*/
public static ItemStack addToItemStack(final ItemStack item, final Map<String, Object> tags, final boolean replace)
throws NMSException {
init();
try {
final ItemStack craftItemStack = (ItemStack) ItemUtils.getCraftItemStack(item);
final Object mcItemStack = ItemUtils.getNMSItemStack(item);
final NBTCompound compound = fromItemStack(craftItemStack);
if (replace) {
compound.clear();
}
compound.putAll(tags);
Object tag = compound.getNbtTagCompound();
if (tag != null) {
final ItemMeta craftItemMeta = (ItemMeta) Reflection
.call(craftItemStack.getClass(), null, "getItemMeta", new Object[] {mcItemStack});
// There's an "applyToItem" method in CraftItemMeta but is doesn't handle well new NBT tags.
// We try to re-create a whole new instance from the same CraftItemMeta base class instead,
// using the constructor accepting a NBTTagCompound.
ItemMeta newCraftItemMeta;
try {
newCraftItemMeta = Reflection.instantiate(craftItemMeta.getClass(), tag);
} catch (NoSuchMethodException e) {
// The CraftMetaBlockState constructor is different (like some Portal's turrets):
// he takes the Material as his second argument.
newCraftItemMeta = Reflection.instantiate(craftItemMeta.getClass(), tag, craftItemStack.getType());
}
craftItemStack.setItemMeta(newCraftItemMeta);
}
return craftItemStack;
} catch (InvocationTargetException | NoSuchMethodException | InstantiationException
| IllegalAccessException | NMSException e) {
throw new NMSException("Cannot set item stack tags", e);
}
}
/**
* Older version, the new addition of prefixes has been made mandatory by 1.17
*/
static Class<?> getMinecraftClass(String className) throws NMSException {
return getMinecraftClass("", className);
}
static Class<?> getMinecraftClass(String prefix, String className) throws NMSException {
try {
return Reflection
.getMinecraft1_17ClassByName(prefix.equals("") ? className : prefix + "." + className); //1.17+
} catch (ClassNotFoundException ex) {
try {
return Reflection.getMinecraftClassByName(className);//Legacy for older version than 1.17
} catch (ClassNotFoundException e) {
throw new NMSException("Unable to find class: " + prefix + className, e);
}
}
}
static Class<?> getCraftBukkitClass(String className) throws NMSException {
try {
return Reflection.getBukkitClassByName(className);
} catch (ClassNotFoundException ex) {
throw new NMSException("Unable to find class: " + className, ex);
}
}
private static void init() throws NMSException {
if (MC_ITEM_STACK != null) {
return; // Already initialized
}
MC_ITEM_STACK = getMinecraftClass("world.item", "ItemStack");
MC_NBT_TAG_COMPOUND = getMinecraftClass("nbt", "NBTTagCompound");
CB_CRAFT_ITEM_META = getCraftBukkitClass("inventory.CraftMetaItem");
}
/**
* Extracts the NBT compound in the given ItemStack.
*
* <p>If there isn't any NBT tag in the ItemStack, one is created, linked to
* the ItemStack, and returned, so this will always return a NBT compound tag
* linked to the ItemStack.</p>
*
* @param item The item to extract NBT tags from.
* @return The NMS NBT tag.
* @throws NMSException If something goes wrong while extracting the tag.
*/
private static Object getMcNBTCompound(ItemStack item) throws NMSException {
Object mcItemStack = ItemUtils.getNMSItemStack(item);
if (mcItemStack == null) {
return null;
}
try {
Object tagCompound;
try {
//1.18
tagCompound = Reflection.call(mcItemStack.getClass(), mcItemStack, "t");
} catch (Exception e) {
//1.17
try {
tagCompound = Reflection.call(mcItemStack.getClass(), mcItemStack, "getTag");
} catch (Exception e2) {
tagCompound = Reflection.call(mcItemStack.getClass(), mcItemStack, "a");
}
}
if (tagCompound == null) {
tagCompound = Reflection.instantiate(MC_NBT_TAG_COMPOUND);
Reflection.call(MC_ITEM_STACK, mcItemStack, "setTag", tagCompound);
}
return tagCompound;
} catch (Exception exc) {
//Older method
try {
Object tag = Reflection.getFieldValue(MC_ITEM_STACK, mcItemStack, "tag");
if (tag == null) {
tag = Reflection.instantiate(MC_NBT_TAG_COMPOUND);
try {
Reflection.call(MC_ITEM_STACK, mcItemStack, "setTag", tag);
} catch (NoSuchMethodException e) {
// If the set method change—more resilient,
// as the setTag will only update the field without any kind of callback.
Reflection.setFieldValue(MC_ITEM_STACK, mcItemStack, "tag", tag);
}
}
return tag;
} catch (Exception ex) {
throw new NMSException("Unable to retrieve NBT tag from item", ex);
}
}
}
/**
* Converts a native Java value to a NMS NBT instance.
*
* @param value The native Java value to convert.
* @return The NMS NBT tag instance converted from the native value.
*/
static Object fromNativeValue(Object value) {
if (value == null) {
return null;
}
NBTType type = NBTType.fromClass(value.getClass());
return type.newTag(value);
}
/**
* Converts a NMS NBT tag instance to a native value, effectively
* unwrapping the value inside to something Java alone can understand.
* <p>Nested values are also converted, if any, to objects implementing
* the {@link Map} or {@link List} interface..</p>
*
* @param nbtTag The NMS NBT tag instance.
* @return The corresponding native value.
*/
static Object toNativeValue(Object nbtTag) {
if (nbtTag == null) {
return null;
}
NBTType type = NBTType.fromNmsNbtTag(nbtTag);
switch (type) {
case TAG_COMPOUND:
return new NBTCompound(nbtTag);
case TAG_LIST:
return new NBTList(nbtTag);
default:
return type.getData(nbtTag);
}
}
/* ========== NBT String Utilities ========== */
/**
* Returns the NBT JSON representation of the given object.
* This method returns a non-strict JSON representation of the object,
* because minecraft (both client and server) can't deal with strict JSON
* for some item nbt tags.
*
* @param value The value to JSONify.
* @return the NBT JSON representation of the given object.
*/
public static String toNBTJSONString(Object value) {
StringBuilder sb = new StringBuilder();
toNBTJSONString(sb, value);
return sb.toString();
}
private static void toNBTJSONString(StringBuilder builder, Object value) {
if (value == null) {
return;
}
if (value instanceof List) {
toNBTJSONString(builder, (List) value);
} else if (value instanceof Map) {
toNBTJSONString(builder, (Map) value);
} else if (value instanceof String) {
toNBTJSONString(builder, (String) value);
} else {
builder.append(value.toString());
}
}
private static void toNBTJSONString(StringBuilder builder, List list) {
builder.append("[");
boolean isFirst = true;
for (Object value : list) {
if (!isFirst) {
builder.append(",");
}
toNBTJSONString(builder, value);
isFirst = false;
}
builder.append("]");
}
private static void toNBTJSONString(StringBuilder builder, Map<Object, Object> map) {
builder.append("{");
boolean isFirst = true;
for (Entry<Object, Object> entry : map.entrySet()) {
if (!isFirst) {
builder.append(",");
}
builder.append(entry.getKey().toString());
builder.append(':');
toNBTJSONString(builder, entry.getValue());
isFirst = false;
}
builder.append("}");
}
private static void toNBTJSONString(StringBuilder builder, String value) {
builder.append('"');
builder.append(value.replace("\"", "\\\""));
builder.append('"');
}
}