-
-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathCustomBlockData.java
More file actions
615 lines (549 loc) · 22.9 KB
/
CustomBlockData.java
File metadata and controls
615 lines (549 loc) · 22.9 KB
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
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
/*
* Copyright (c) 2022 Alexander Majka (mfnalex) / JEFF Media GbR
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License.
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* If you need help or have any suggestions, feel free to join my Discord and head to #programming-help:
*
* Discord: https://discord.jeff-media.com/
*
* If you find this library helpful or if you're using it one of your paid plugins, please consider leaving a donation
* to support the further development of this project :)
*
* Donations: https://paypal.me/mfnalex
*/
package com.jeff_media.customblockdata;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.persistence.PersistentDataAdapterContext;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.BlockVector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.jeff_media.customblockdata.events.CustomBlockDataSavedEvent;
/**
* Represents a {@link PersistentDataContainer} for a specific {@link Block}. Also provides some static utility methods
* that can be used on every PersistentDataContainer.
* <p>
* By default, and for backward compatibility reasons, data stored inside blocks is independent of the underlying block.
* That means: if you store some data inside a dirt block, and that block is now pushed by a piston, then the information
* will still reside in the old block's location. <b>You can of course also make CustomBockData automatically take care of those situations</b>,
* so that CustomBlockData will always be updated on certain Bukkit Events like BlockBreakEvent, EntityExplodeEvent, etc.
* For more information about this please see {@link #registerListener(Plugin)}.
*/
public class CustomBlockData implements PersistentDataContainer {
/**
* The default package name that must be changed
*/
private static final char[] DEFAULT_PACKAGE = new char[]{'c', 'o', 'm', '.', 'j', 'e', 'f', 'f', '_', 'm', 'e', 'd', 'i', 'a', '.', 'c', 'u', 's', 't', 'o', 'm', 'b', 'l', 'o', 'c', 'k', 'd', 'a', 't', 'a'};
/**
* Set of "dirty block positions", that is blocks that have been modified and need to be saved to the chunk
*/
private static final Set<Map.Entry<UUID, BlockVector>> DIRTY_BLOCKS = new HashSet<>();
/**
* Builtin list of native PersistentDataTypes
*/
private static final PersistentDataType<?, ?>[] PRIMITIVE_DATA_TYPES = new PersistentDataType<?, ?>[]{
PersistentDataType.BYTE,
PersistentDataType.SHORT,
PersistentDataType.INTEGER,
PersistentDataType.LONG,
PersistentDataType.FLOAT,
PersistentDataType.DOUBLE,
PersistentDataType.STRING,
PersistentDataType.BYTE_ARRAY,
PersistentDataType.INTEGER_ARRAY,
PersistentDataType.LONG_ARRAY,
PersistentDataType.TAG_CONTAINER_ARRAY,
PersistentDataType.TAG_CONTAINER};
/**
* NamespacedKey for the CustomBlockData "protected" key
*/
private static final NamespacedKey PERSISTENCE_KEY = Objects.requireNonNull(NamespacedKey.fromString("customblockdata:protected"), "Could not create persistence NamespacedKey");
/**
* Regex used to identify valid CustomBlockData keys
*/
private static final Pattern KEY_REGEX = Pattern.compile("^x(\\d+)y(-?\\d+)z(\\d+)$");
/**
* The minimum X and Z coordinate of any block inside a chunk.
*/
private static final int CHUNK_MIN_XZ = 0;
/**
* The maximum X and Z coordinate of any block inside a chunk.
*/
private static final int CHUNK_MAX_XZ = (2 << 3) -1;
/**
* Whether WorldInfo#getMinHeight() method exists. In some very specific versions, it's directly declared in World.
*/
private static final boolean HAS_MIN_HEIGHT_METHOD;
private static boolean onFolia;
static {
checkRelocation();
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
onFolia = true;
} catch (ClassNotFoundException e) {
onFolia = false;
}
}
static {
boolean tmpHasMinHeightMethod = false;
try {
// Usually declared in WorldInfo, which World extends - except for some very specific versions
World.class.getMethod("getMinHeight");
tmpHasMinHeightMethod = true;
} catch (final ReflectiveOperationException ignored) {
}
HAS_MIN_HEIGHT_METHOD = tmpHasMinHeightMethod;
}
/**
* The Chunk PDC belonging to this CustomBlockData object
*/
private final PersistentDataContainer pdc;
/**
* The Chunk this CustomBlockData object belongs to
*/
private final Chunk chunk;
/**
* The NamespacedKey used to identify this CustomBlockData object inside the Chunk's PDC
*/
private final NamespacedKey key;
/**
* The Map.Entry containing the UUID of the block and its BlockVector for usage with {@link #DIRTY_BLOCKS}
*/
private final Map.Entry<UUID, BlockVector> blockEntry;
/**
* The Plugin this CustomBlockData object belongs to
*/
private final Plugin plugin;
/**
* Gets the PersistentDataContainer associated with the given block and plugin
*
* @param block Block
* @param plugin Plugin
*/
public CustomBlockData(final @NotNull Block block, final @NotNull Plugin plugin) {
this.chunk = block.getChunk();
this.key = getKey(plugin, block);
this.pdc = getPersistentDataContainer();
this.blockEntry = getBlockEntry(block);
this.plugin = plugin;
}
/**
* Gets the PersistentDataContainer associated with the given block and plugin
*
* @param block Block
* @param namespace Namespace
* @deprecated Use {@link #CustomBlockData(Block, Plugin)} instead.
*/
@Deprecated
public CustomBlockData(final @NotNull Block block, final @NotNull String namespace) {
this.chunk = block.getChunk();
this.key = new NamespacedKey(namespace, getKey(block));
this.pdc = getPersistentDataContainer();
this.plugin = JavaPlugin.getProvidingPlugin(CustomBlockData.class);
this.blockEntry = getBlockEntry(block);
}
/**
* Prints a nag message when the CustomBlockData package is not relocated
*/
private static void checkRelocation() {
if (CustomBlockData.class.getPackage().getName().equals(new String(DEFAULT_PACKAGE))) {
try {
JavaPlugin plugin = JavaPlugin.getProvidingPlugin(CustomBlockData.class);
plugin.getLogger().warning("Nag author(s) " + String.join(", ", plugin.getDescription().getAuthors()) + " of plugin " + plugin.getName() + " for not relocating the CustomBlockData package.");
} catch (IllegalArgumentException exception) {
// Could not get plugin
}
}
}
/**
* Gets the block entry for this block used for {@link #DIRTY_BLOCKS}
* @param block Block
* @return Block entry
*/
private static Map.Entry<UUID, BlockVector> getBlockEntry(final @NotNull Block block) {
final UUID uuid = block.getWorld().getUID();
final BlockVector blockVector = new BlockVector(block.getX(), block.getY(), block.getZ());
return new AbstractMap.SimpleEntry<>(uuid, blockVector);
}
/**
* Checks whether this block is flagged as "dirty"
* @param block Block
* @return Whether this block is flagged as "dirty"
*/
static boolean isDirty(Block block) {
return DIRTY_BLOCKS.contains(getBlockEntry(block));
}
/**
* Sets this block as "dirty" and removes it from the list after the next tick.
* <p>
* If the plugin is disabled, this method will do nothing, to prevent the IllegalPluginAccessException.
* @param plugin Plugin
* @param blockEntry Block entry
*/
static void setDirty(Plugin plugin, Map.Entry<UUID, BlockVector> blockEntry) {
if (!plugin.isEnabled()) //checks if the plugin is disabled to prevent the IllegalPluginAccessException
return;
DIRTY_BLOCKS.add(blockEntry);
if (onFolia) {
Bukkit.getServer().getGlobalRegionScheduler().runDelayed(plugin, task -> {
DIRTY_BLOCKS.remove(blockEntry);
}, 1L);
} else {
Bukkit.getScheduler().runTask(plugin, () -> DIRTY_BLOCKS.remove(blockEntry));
}
}
/**
* Gets the NamespacedKey for this block
* @param plugin Plugin
* @param block Block
* @return NamespacedKey
*/
private static NamespacedKey getKey(Plugin plugin, Block block) {
return new NamespacedKey(plugin, getKey(block));
}
/**
* Gets a String-based {@link NamespacedKey} that consists of the block's relative coordinates within its chunk
*
* @param block block
* @return NamespacedKey consisting of the block's relative coordinates within its chunk
*/
@NotNull
static String getKey(@NotNull final Block block) {
final int x = block.getX() & 0x000F;
final int y = block.getY();
final int z = block.getZ() & 0x000F;
return "x" + x + "y" + y + "z" + z;
}
/**
* Gets the block represented by the given {@link NamespacedKey} in the given {@link Chunk}
*/
@Nullable
static Block getBlockFromKey(final NamespacedKey key, final Chunk chunk) {
final Matcher matcher = KEY_REGEX.matcher(key.getKey());
if (!matcher.matches()) return null;
final int x = Integer.parseInt(matcher.group(1));
final int y = Integer.parseInt(matcher.group(2));
final int z = Integer.parseInt(matcher.group(3));
if ((x < CHUNK_MIN_XZ || x > CHUNK_MAX_XZ) || (z < CHUNK_MIN_XZ || z > CHUNK_MAX_XZ) || (y < getWorldMinHeight(chunk.getWorld()) || y > chunk.getWorld().getMaxHeight() - 1))
return null;
return chunk.getBlock(x, y, z);
}
/**
* Returns the given {@link World}'s minimum build height, or 0 if not supported in this Bukkit version
*/
static int getWorldMinHeight(final World world) {
if (HAS_MIN_HEIGHT_METHOD) {
return world.getMinHeight();
} else {
return 0;
}
}
/**
* Get if the given Block has any CustomBockData associated with it
*/
public static boolean hasCustomBlockData(Block block, Plugin plugin) {
return block.getChunk().getPersistentDataContainer().has(getKey(plugin, block), PersistentDataType.TAG_CONTAINER);
}
/**
* Get if the given Block's CustomBlockData is protected. Protected CustomBlockData will not be changed by any Bukkit Events
*
* @return true if the Block's CustomBlockData is protected, false if it doesn't have any CustomBlockData or it's not protected
* @see #registerListener(Plugin)
*/
public static boolean isProtected(Block block, Plugin plugin) {
return new CustomBlockData(block, plugin).isProtected();
}
/**
* Starts to listen and manage block-related events such as {@link BlockBreakEvent}. By default, CustomBlockData
* is "stateless". That means: when you add data to a block, and now a player breaks the block, the data will
* still reside at the original block location. This is to ensure that you always have full control about what data
* is saved at which location.
* <p>
* If you do not want to handle this yourself, you can instead let CustomBlockData handle those events by calling this
* method once. It will then listen to the common events itself, and automatically remove/update CustomBlockData.
* <p>
* Block changes made using the Bukkit API (e.g. {@link Block#setType(Material)}) or using a plugin like WorldEdit
* will <b>not</b> be registered by this (but pull requests are welcome, of course)
* <p>
* For example, when you call this method in onEnable, CustomBlockData will now get automatically removed from a block
* when a player breaks this block. It will additionally call custom events like {@link com.jeff_media.customblockdata.events.CustomBlockDataRemoveEvent}.
* Those events implement {@link org.bukkit.event.Cancellable}. If one of the CustomBlockData events is cancelled,
* it will not alter any CustomBlockData.
*
* @param plugin Your plugin's instance
*/
public static void registerListener(Plugin plugin) {
Bukkit.getPluginManager().registerEvents(new BlockDataListener(plugin), plugin);
}
/**
* Returns a Set of all blocks in this chunk containing Custom Block Data created by the given plugin
*
* @param plugin Plugin
* @param chunk Chunk
* @return A Set containing all blocks in this chunk containing Custom Block Data created by the given plugin
*/
@NotNull
public static Set<Block> getBlocksWithCustomData(final Plugin plugin, final Chunk chunk) {
final NamespacedKey dummy = new NamespacedKey(plugin, "dummy");
return getBlocksWithCustomData(chunk, dummy);
}
/**
* Returns a {@link Set} of all blocks in this {@link Chunk} containing Custom Block Data matching the given {@link NamespacedKey}'s namespace
*
* @param namespace Namespace
* @param chunk Chunk
* @return A {@link Set} containing all blocks in this chunk containing Custom Block Data created by the given plugin
*/
@NotNull
private static Set<Block> getBlocksWithCustomData(final @NotNull Chunk chunk, final @NotNull NamespacedKey namespace) {
final PersistentDataContainer chunkPDC = chunk.getPersistentDataContainer();
return chunkPDC.getKeys().stream().filter(key -> key.getNamespace().equals(namespace.getNamespace()))
.map(key -> getBlockFromKey(key, chunk))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
/**
* Returns a {@link Set} of all blocks in this {@link Chunk} containing Custom Block Data created by the given plugin
*
* @param namespace Namespace
* @param chunk Chunk
* @return A {@link Set} containing all blocks in this chunk containing Custom Block Data created by the given plugin
*/
@NotNull
public static Set<Block> getBlocksWithCustomData(final String namespace, final Chunk chunk) {
@SuppressWarnings("deprecation") final NamespacedKey dummy = new NamespacedKey(namespace, "dummy");
return getBlocksWithCustomData(chunk, dummy);
}
/**
* Gets the proper primitive {@link PersistentDataType} for the given {@link NamespacedKey} in the given {@link PersistentDataContainer}
*
* @return The primitive PersistentDataType for the given key, or null if the key doesn't exist
*/
public static PersistentDataType<?, ?> getDataType(PersistentDataContainer pdc, NamespacedKey key) {
for (PersistentDataType<?, ?> dataType : PRIMITIVE_DATA_TYPES) {
if (pdc.has(key, dataType)) return dataType;
}
return null;
}
/**
* Gets the Block associated with this CustomBlockData, or null if the world is no longer loaded.
*/
public @Nullable Block getBlock() {
World world = Bukkit.getWorld(blockEntry.getKey());
if (world == null) return null;
BlockVector vector = blockEntry.getValue();
return world.getBlockAt(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ());
}
/**
* Gets the PersistentDataContainer associated with this block.
*
* @return PersistentDataContainer of this block
*/
@NotNull
private PersistentDataContainer getPersistentDataContainer() {
final PersistentDataContainer chunkPDC = chunk.getPersistentDataContainer();
final PersistentDataContainer blockPDC = chunkPDC.get(key, PersistentDataType.TAG_CONTAINER);
if (blockPDC != null) return blockPDC;
return chunkPDC.getAdapterContext().newPersistentDataContainer();
}
/**
* Gets whether this CustomBlockData is protected. Protected CustomBlockData will not be changed by any Bukkit Events
*
* @see #registerListener(Plugin)
*/
public boolean isProtected() {
return has(PERSISTENCE_KEY, DataType.BOOLEAN);
}
/**
* Sets whether this CustomBlockData is protected. Protected CustomBlockData will not be changed by any Bukkit Events
*
* @see #registerListener(Plugin)
*/
public void setProtected(boolean isProtected) {
if (isProtected) {
set(PERSISTENCE_KEY, DataType.BOOLEAN, true);
} else {
remove(PERSISTENCE_KEY);
}
}
/**
* Removes all CustomBlockData and disables the protection status ({@link #setProtected(boolean)}
*/
public void clear() {
pdc.getKeys().forEach(pdc::remove);
save();
}
/**
* Saves the block's {@link PersistentDataContainer} inside the chunk's PersistentDataContainer.
* Fires the {@link CustomBlockDataSavedEvent} event after saving.
*/
private void save() {
setDirty(plugin, blockEntry);
if (pdc.isEmpty()) {
chunk.getPersistentDataContainer().remove(key);
} else {
chunk.getPersistentDataContainer().set(key, PersistentDataType.TAG_CONTAINER, pdc);
}
CustomBlockDataSavedEvent event = new CustomBlockDataSavedEvent(this);
Bukkit.getPluginManager().callEvent(event);
}
/**
* Copies all data to another block. Data already present in the destination block will keep intact, unless it gets
* overwritten by identically named keys. Data in the source block won't be changed.
*/
@SuppressWarnings({"unchecked", "rawtypes", "ConstantConditions"})
public void copyTo(Block block, Plugin plugin) {
CustomBlockData newCbd = new CustomBlockData(block, plugin);
getKeys().forEach(key -> {
PersistentDataType dataType = getDataType(this, key);
if (dataType == null) return;
newCbd.set(key, dataType, get(key, dataType));
});
}
@Override
public <T, Z> void set(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType<T, Z> persistentDataType, final @NotNull Z z) {
pdc.set(namespacedKey, persistentDataType, z);
save();
}
@Override
public <T, Z> boolean has(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType<T, Z> persistentDataType) {
return pdc.has(namespacedKey, persistentDataType);
}
@Override
public boolean has(final @NotNull NamespacedKey namespacedKey) {
for (PersistentDataType<?, ?> type : PRIMITIVE_DATA_TYPES) {
if (pdc.has(namespacedKey, type)) return true;
}
return false;
}
@Nullable
@Override
public <T, Z> Z get(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType<T, Z> persistentDataType) {
return pdc.get(namespacedKey, persistentDataType);
}
@NotNull
@Override
public <T, Z> Z getOrDefault(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType<T, Z> persistentDataType, final @NotNull Z z) {
return pdc.getOrDefault(namespacedKey, persistentDataType, z);
}
@NotNull
@Override
public Set<NamespacedKey> getKeys() {
return pdc.getKeys();
}
@Override
public void remove(final @NotNull NamespacedKey namespacedKey) {
pdc.remove(namespacedKey);
save();
}
@Override
public boolean isEmpty() {
return pdc.isEmpty();
}
@NotNull
@Override
public PersistentDataAdapterContext getAdapterContext() {
return pdc.getAdapterContext();
}
/**
* @see PersistentDataContainer#serializeToBytes()
* @deprecated Paper-only
*/
@NotNull
@Override
@PaperOnly
@Deprecated
public byte[] serializeToBytes() throws IOException {
return pdc.serializeToBytes();
}
/**
* @see PersistentDataContainer#readFromBytes(byte[], boolean) ()
* @deprecated Paper-only
*/
@Override
@PaperOnly
@Deprecated
public void readFromBytes(byte[] bytes, boolean clear) throws IOException {
pdc.readFromBytes(bytes, clear);
}
/**
* @see PersistentDataContainer#readFromBytes(byte[]) ()
* @deprecated Paper-only
*/
@Override
@PaperOnly
@Deprecated
public void readFromBytes(byte[] bytes) throws IOException {
pdc.readFromBytes(bytes);
}
/**
* Gets the proper primitive {@link PersistentDataType} for the given {@link NamespacedKey}
*
* @return The primitive PersistentDataType for the given key, or null if the key doesn't exist
*/
public PersistentDataType<?, ?> getDataType(NamespacedKey key) {
return getDataType(this, key);
}
/**
* Indicates a method that only works on Paper and forks, but not on Spigot or CraftBukkit
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
private @interface PaperOnly {
}
private static final class DataType {
private static final PersistentDataType<Byte, Boolean> BOOLEAN = new PersistentDataType<Byte, Boolean>() {
@NotNull
@Override
public Class<Byte> getPrimitiveType() {
return Byte.class;
}
@NotNull
@Override
public Class<Boolean> getComplexType() {
return Boolean.class;
}
@NotNull
@Override
public Byte toPrimitive(@NotNull Boolean complex, @NotNull PersistentDataAdapterContext context) {
return complex ? (byte) 1 : (byte) 0;
}
@NotNull
@Override
public Boolean fromPrimitive(@NotNull Byte primitive, @NotNull PersistentDataAdapterContext context) {
return primitive == (byte) 1;
}
};
}
}