keys = new ArrayList<>(map.keySet());
194 | Collections.reverse(keys);
195 | keys.forEach((key) -> reversed.put(key, map.get(key)));
196 | return reversed;
197 | }
198 |
199 | }
200 |
201 | }
202 |
--------------------------------------------------------------------------------
/src/main/java/com/jeff_media/customblockdata/CustomBlockData.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Alexander Majka (mfnalex) / JEFF Media GbR
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | *
13 | * If you need help or have any suggestions, feel free to join my Discord and head to #programming-help:
14 | *
15 | * Discord: https://discord.jeff-media.com/
16 | *
17 | * If you find this library helpful or if you're using it one of your paid plugins, please consider leaving a donation
18 | * to support the further development of this project :)
19 | *
20 | * Donations: https://paypal.me/mfnalex
21 | */
22 |
23 | package com.jeff_media.customblockdata;
24 |
25 | import org.bukkit.*;
26 | import org.bukkit.block.Block;
27 | import org.bukkit.event.block.BlockBreakEvent;
28 | import org.bukkit.persistence.PersistentDataAdapterContext;
29 | import org.bukkit.persistence.PersistentDataContainer;
30 | import org.bukkit.persistence.PersistentDataType;
31 | import org.bukkit.plugin.Plugin;
32 | import org.bukkit.plugin.java.JavaPlugin;
33 | import org.bukkit.util.BlockVector;
34 | import org.jetbrains.annotations.NotNull;
35 | import org.jetbrains.annotations.Nullable;
36 |
37 | import java.io.IOException;
38 | import java.lang.annotation.ElementType;
39 | import java.lang.annotation.Retention;
40 | import java.lang.annotation.RetentionPolicy;
41 | import java.lang.annotation.Target;
42 | import java.util.*;
43 | import java.util.regex.Matcher;
44 | import java.util.regex.Pattern;
45 | import java.util.stream.Collectors;
46 |
47 | /**
48 | * Represents a {@link PersistentDataContainer} for a specific {@link Block}. Also provides some static utility methods
49 | * that can be used on every PersistentDataContainer.
50 | *
51 | * By default, and for backward compatibility reasons, data stored inside blocks is independent of the underlying block.
52 | * That means: if you store some data inside a dirt block, and that block is now pushed by a piston, then the information
53 | * will still reside in the old block's location. You can of course also make CustomBockData automatically take care of those situations,
54 | * so that CustomBlockData will always be updated on certain Bukkit Events like BlockBreakEvent, EntityExplodeEvent, etc.
55 | * For more information about this please see {@link #registerListener(Plugin)}.
56 | */
57 | public class CustomBlockData implements PersistentDataContainer {
58 |
59 | /**
60 | * The default package name that must be changed
61 | */
62 | 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'};
63 |
64 | /**
65 | * Set of "dirty block positions", that is blocks that have been modified and need to be saved to the chunk
66 | */
67 | private static final Set> DIRTY_BLOCKS = new HashSet<>();
68 |
69 | /**
70 | * Builtin list of native PersistentDataTypes
71 | */
72 | private static final PersistentDataType, ?>[] PRIMITIVE_DATA_TYPES = new PersistentDataType, ?>[]{
73 | PersistentDataType.BYTE,
74 | PersistentDataType.SHORT,
75 | PersistentDataType.INTEGER,
76 | PersistentDataType.LONG,
77 | PersistentDataType.FLOAT,
78 | PersistentDataType.DOUBLE,
79 | PersistentDataType.STRING,
80 | PersistentDataType.BYTE_ARRAY,
81 | PersistentDataType.INTEGER_ARRAY,
82 | PersistentDataType.LONG_ARRAY,
83 | PersistentDataType.TAG_CONTAINER_ARRAY,
84 | PersistentDataType.TAG_CONTAINER};
85 |
86 | /**
87 | * NamespacedKey for the CustomBlockData "protected" key
88 | */
89 | private static final NamespacedKey PERSISTENCE_KEY = Objects.requireNonNull(NamespacedKey.fromString("customblockdata:protected"), "Could not create persistence NamespacedKey");
90 |
91 | /**
92 | * Regex used to identify valid CustomBlockData keys
93 | */
94 | private static final Pattern KEY_REGEX = Pattern.compile("^x(\\d+)y(-?\\d+)z(\\d+)$");
95 |
96 | /**
97 | * The minimum X and Z coordinate of any block inside a chunk.
98 | */
99 | private static final int CHUNK_MIN_XZ = 0;
100 |
101 | /**
102 | * The maximum X and Z coordinate of any block inside a chunk.
103 | */
104 | private static final int CHUNK_MAX_XZ = (2 << 3) -1;
105 |
106 | /**
107 | * Whether WorldInfo#getMinHeight() method exists. In some very specific versions, it's directly declared in World.
108 | */
109 | private static final boolean HAS_MIN_HEIGHT_METHOD;
110 |
111 | private static boolean onFolia;
112 |
113 | static {
114 | checkRelocation();
115 | try {
116 | Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
117 | onFolia = true;
118 | } catch (ClassNotFoundException e) {
119 | onFolia = false;
120 | }
121 | }
122 |
123 | static {
124 | boolean tmpHasMinHeightMethod = false;
125 | try {
126 | // Usually declared in WorldInfo, which World extends - except for some very specific versions
127 | World.class.getMethod("getMinHeight");
128 | tmpHasMinHeightMethod = true;
129 | } catch (final ReflectiveOperationException ignored) {
130 | }
131 | HAS_MIN_HEIGHT_METHOD = tmpHasMinHeightMethod;
132 | }
133 |
134 | /**
135 | * The Chunk PDC belonging to this CustomBlockData object
136 | */
137 | private final PersistentDataContainer pdc;
138 |
139 | /**
140 | * The Chunk this CustomBlockData object belongs to
141 | */
142 | private final Chunk chunk;
143 |
144 | /**
145 | * The NamespacedKey used to identify this CustomBlockData object inside the Chunk's PDC
146 | */
147 | private final NamespacedKey key;
148 |
149 | /**
150 | * The Map.Entry containing the UUID of the block and its BlockVector for usage with {@link #DIRTY_BLOCKS}
151 | */
152 | private final Map.Entry blockEntry;
153 |
154 | /**
155 | * The Plugin this CustomBlockData object belongs to
156 | */
157 | private final Plugin plugin;
158 |
159 | /**
160 | * Gets the PersistentDataContainer associated with the given block and plugin
161 | *
162 | * @param block Block
163 | * @param plugin Plugin
164 | */
165 | public CustomBlockData(final @NotNull Block block, final @NotNull Plugin plugin) {
166 | this.chunk = block.getChunk();
167 | this.key = getKey(plugin, block);
168 | this.pdc = getPersistentDataContainer();
169 | this.blockEntry = getBlockEntry(block);
170 | this.plugin = plugin;
171 | }
172 |
173 | /**
174 | * Gets the PersistentDataContainer associated with the given block and plugin
175 | *
176 | * @param block Block
177 | * @param namespace Namespace
178 | * @deprecated Use {@link #CustomBlockData(Block, Plugin)} instead.
179 | */
180 | @Deprecated
181 | public CustomBlockData(final @NotNull Block block, final @NotNull String namespace) {
182 | this.chunk = block.getChunk();
183 | this.key = new NamespacedKey(namespace, getKey(block));
184 | this.pdc = getPersistentDataContainer();
185 | this.plugin = JavaPlugin.getProvidingPlugin(CustomBlockData.class);
186 | this.blockEntry = getBlockEntry(block);
187 | }
188 |
189 | /**
190 | * Prints a nag message when the CustomBlockData package is not relocated
191 | */
192 | private static void checkRelocation() {
193 | if (CustomBlockData.class.getPackage().getName().equals(new String(DEFAULT_PACKAGE))) {
194 | try {
195 | JavaPlugin plugin = JavaPlugin.getProvidingPlugin(CustomBlockData.class);
196 | plugin.getLogger().warning("Nag author(s) " + String.join(", ", plugin.getDescription().getAuthors()) + " of plugin " + plugin.getName() + " for not relocating the CustomBlockData package.");
197 | } catch (IllegalArgumentException exception) {
198 | // Could not get plugin
199 | }
200 | }
201 | }
202 |
203 | /**
204 | * Gets the block entry for this block used for {@link #DIRTY_BLOCKS}
205 | * @param block Block
206 | * @return Block entry
207 | */
208 | private static Map.Entry getBlockEntry(final @NotNull Block block) {
209 | final UUID uuid = block.getWorld().getUID();
210 | final BlockVector blockVector = new BlockVector(block.getX(), block.getY(), block.getZ());
211 | return new AbstractMap.SimpleEntry<>(uuid, blockVector);
212 | }
213 |
214 | /**
215 | * Checks whether this block is flagged as "dirty"
216 | * @param block Block
217 | * @return Whether this block is flagged as "dirty"
218 | */
219 | static boolean isDirty(Block block) {
220 | return DIRTY_BLOCKS.contains(getBlockEntry(block));
221 | }
222 |
223 | /**
224 | * Sets this block as "dirty" and removes it from the list after the next tick.
225 | *
226 | * If the plugin is disabled, this method will do nothing, to prevent the IllegalPluginAccessException.
227 | * @param plugin Plugin
228 | * @param blockEntry Block entry
229 | */
230 | static void setDirty(Plugin plugin, Map.Entry blockEntry) {
231 | if (!plugin.isEnabled()) //checks if the plugin is disabled to prevent the IllegalPluginAccessException
232 | return;
233 |
234 | DIRTY_BLOCKS.add(blockEntry);
235 | if (onFolia) {
236 | Bukkit.getServer().getGlobalRegionScheduler().runDelayed(plugin, task -> {
237 | DIRTY_BLOCKS.remove(blockEntry);
238 | }, 1L);
239 | } else {
240 | Bukkit.getScheduler().runTask(plugin, () -> DIRTY_BLOCKS.remove(blockEntry));
241 | }
242 | }
243 |
244 | /**
245 | * Gets the NamespacedKey for this block
246 | * @param plugin Plugin
247 | * @param block Block
248 | * @return NamespacedKey
249 | */
250 | private static NamespacedKey getKey(Plugin plugin, Block block) {
251 | return new NamespacedKey(plugin, getKey(block));
252 | }
253 |
254 | /**
255 | * Gets a String-based {@link NamespacedKey} that consists of the block's relative coordinates within its chunk
256 | *
257 | * @param block block
258 | * @return NamespacedKey consisting of the block's relative coordinates within its chunk
259 | */
260 | @NotNull
261 | static String getKey(@NotNull final Block block) {
262 | final int x = block.getX() & 0x000F;
263 | final int y = block.getY();
264 | final int z = block.getZ() & 0x000F;
265 | return "x" + x + "y" + y + "z" + z;
266 | }
267 |
268 | /**
269 | * Gets the block represented by the given {@link NamespacedKey} in the given {@link Chunk}
270 | */
271 | @Nullable
272 | static Block getBlockFromKey(final NamespacedKey key, final Chunk chunk) {
273 | final Matcher matcher = KEY_REGEX.matcher(key.getKey());
274 | if (!matcher.matches()) return null;
275 | final int x = Integer.parseInt(matcher.group(1));
276 | final int y = Integer.parseInt(matcher.group(2));
277 | final int z = Integer.parseInt(matcher.group(3));
278 | 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))
279 | return null;
280 | return chunk.getBlock(x, y, z);
281 | }
282 |
283 | /**
284 | * Returns the given {@link World}'s minimum build height, or 0 if not supported in this Bukkit version
285 | */
286 | static int getWorldMinHeight(final World world) {
287 | if (HAS_MIN_HEIGHT_METHOD) {
288 | return world.getMinHeight();
289 | } else {
290 | return 0;
291 | }
292 | }
293 |
294 | /**
295 | * Get if the given Block has any CustomBockData associated with it
296 | */
297 | public static boolean hasCustomBlockData(Block block, Plugin plugin) {
298 | return block.getChunk().getPersistentDataContainer().has(getKey(plugin, block), PersistentDataType.TAG_CONTAINER);
299 | }
300 |
301 | /**
302 | * Get if the given Block's CustomBlockData is protected. Protected CustomBlockData will not be changed by any Bukkit Events
303 | *
304 | * @return true if the Block's CustomBlockData is protected, false if it doesn't have any CustomBlockData or it's not protected
305 | * @see #registerListener(Plugin)
306 | */
307 | public static boolean isProtected(Block block, Plugin plugin) {
308 | return new CustomBlockData(block, plugin).isProtected();
309 | }
310 |
311 | /**
312 | * Starts to listen and manage block-related events such as {@link BlockBreakEvent}. By default, CustomBlockData
313 | * is "stateless". That means: when you add data to a block, and now a player breaks the block, the data will
314 | * still reside at the original block location. This is to ensure that you always have full control about what data
315 | * is saved at which location.
316 | *
317 | * If you do not want to handle this yourself, you can instead let CustomBlockData handle those events by calling this
318 | * method once. It will then listen to the common events itself, and automatically remove/update CustomBlockData.
319 | *
320 | * Block changes made using the Bukkit API (e.g. {@link Block#setType(Material)}) or using a plugin like WorldEdit
321 | * will not be registered by this (but pull requests are welcome, of course)
322 | *
323 | * For example, when you call this method in onEnable, CustomBlockData will now get automatically removed from a block
324 | * when a player breaks this block. It will additionally call custom events like {@link com.jeff_media.customblockdata.events.CustomBlockDataRemoveEvent}.
325 | * Those events implement {@link org.bukkit.event.Cancellable}. If one of the CustomBlockData events is cancelled,
326 | * it will not alter any CustomBlockData.
327 | *
328 | * @param plugin Your plugin's instance
329 | */
330 | public static void registerListener(Plugin plugin) {
331 | Bukkit.getPluginManager().registerEvents(new BlockDataListener(plugin), plugin);
332 | }
333 |
334 | /**
335 | * Returns a Set of all blocks in this chunk containing Custom Block Data created by the given plugin
336 | *
337 | * @param plugin Plugin
338 | * @param chunk Chunk
339 | * @return A Set containing all blocks in this chunk containing Custom Block Data created by the given plugin
340 | */
341 | @NotNull
342 | public static Set getBlocksWithCustomData(final Plugin plugin, final Chunk chunk) {
343 | final NamespacedKey dummy = new NamespacedKey(plugin, "dummy");
344 | return getBlocksWithCustomData(chunk, dummy);
345 | }
346 |
347 | /**
348 | * Returns a {@link Set} of all blocks in this {@link Chunk} containing Custom Block Data matching the given {@link NamespacedKey}'s namespace
349 | *
350 | * @param namespace Namespace
351 | * @param chunk Chunk
352 | * @return A {@link Set} containing all blocks in this chunk containing Custom Block Data created by the given plugin
353 | */
354 | @NotNull
355 | private static Set getBlocksWithCustomData(final @NotNull Chunk chunk, final @NotNull NamespacedKey namespace) {
356 | final PersistentDataContainer chunkPDC = chunk.getPersistentDataContainer();
357 | return chunkPDC.getKeys().stream().filter(key -> key.getNamespace().equals(namespace.getNamespace()))
358 | .map(key -> getBlockFromKey(key, chunk))
359 | .filter(Objects::nonNull)
360 | .collect(Collectors.toSet());
361 | }
362 |
363 | /**
364 | * Returns a {@link Set} of all blocks in this {@link Chunk} containing Custom Block Data created by the given plugin
365 | *
366 | * @param namespace Namespace
367 | * @param chunk Chunk
368 | * @return A {@link Set} containing all blocks in this chunk containing Custom Block Data created by the given plugin
369 | */
370 | @NotNull
371 | public static Set getBlocksWithCustomData(final String namespace, final Chunk chunk) {
372 | @SuppressWarnings("deprecation") final NamespacedKey dummy = new NamespacedKey(namespace, "dummy");
373 | return getBlocksWithCustomData(chunk, dummy);
374 | }
375 |
376 | /**
377 | * Gets the proper primitive {@link PersistentDataType} for the given {@link NamespacedKey} in the given {@link PersistentDataContainer}
378 | *
379 | * @return The primitive PersistentDataType for the given key, or null if the key doesn't exist
380 | */
381 | public static PersistentDataType, ?> getDataType(PersistentDataContainer pdc, NamespacedKey key) {
382 | for (PersistentDataType, ?> dataType : PRIMITIVE_DATA_TYPES) {
383 | if (pdc.has(key, dataType)) return dataType;
384 | }
385 | return null;
386 | }
387 |
388 | /**
389 | * Gets the Block associated with this CustomBlockData, or null if the world is no longer loaded.
390 | */
391 | public @Nullable Block getBlock() {
392 | World world = Bukkit.getWorld(blockEntry.getKey());
393 | if (world == null) return null;
394 | BlockVector vector = blockEntry.getValue();
395 | return world.getBlockAt(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ());
396 | }
397 |
398 | /**
399 | * Gets the PersistentDataContainer associated with this block.
400 | *
401 | * @return PersistentDataContainer of this block
402 | */
403 | @NotNull
404 | private PersistentDataContainer getPersistentDataContainer() {
405 | final PersistentDataContainer chunkPDC = chunk.getPersistentDataContainer();
406 | final PersistentDataContainer blockPDC = chunkPDC.get(key, PersistentDataType.TAG_CONTAINER);
407 | if (blockPDC != null) return blockPDC;
408 | return chunkPDC.getAdapterContext().newPersistentDataContainer();
409 | }
410 |
411 | /**
412 | * Gets whether this CustomBlockData is protected. Protected CustomBlockData will not be changed by any Bukkit Events
413 | *
414 | * @see #registerListener(Plugin)
415 | */
416 | public boolean isProtected() {
417 | return has(PERSISTENCE_KEY, DataType.BOOLEAN);
418 | }
419 |
420 | /**
421 | * Sets whether this CustomBlockData is protected. Protected CustomBlockData will not be changed by any Bukkit Events
422 | *
423 | * @see #registerListener(Plugin)
424 | */
425 | public void setProtected(boolean isProtected) {
426 | if (isProtected) {
427 | set(PERSISTENCE_KEY, DataType.BOOLEAN, true);
428 | } else {
429 | remove(PERSISTENCE_KEY);
430 | }
431 | }
432 |
433 | /**
434 | * Removes all CustomBlockData and disables the protection status ({@link #setProtected(boolean)}
435 | */
436 | public void clear() {
437 | pdc.getKeys().forEach(pdc::remove);
438 | save();
439 | }
440 |
441 | /**
442 | * Saves the block's {@link PersistentDataContainer} inside the chunk's PersistentDataContainer
443 | */
444 | private void save() {
445 | setDirty(plugin, blockEntry);
446 | if (pdc.isEmpty()) {
447 | chunk.getPersistentDataContainer().remove(key);
448 | } else {
449 | chunk.getPersistentDataContainer().set(key, PersistentDataType.TAG_CONTAINER, pdc);
450 | }
451 | }
452 |
453 | /**
454 | * Copies all data to another block. Data already present in the destination block will keep intact, unless it gets
455 | * overwritten by identically named keys. Data in the source block won't be changed.
456 | */
457 | @SuppressWarnings({"unchecked", "rawtypes", "ConstantConditions"})
458 | public void copyTo(Block block, Plugin plugin) {
459 | CustomBlockData newCbd = new CustomBlockData(block, plugin);
460 | getKeys().forEach(key -> {
461 | PersistentDataType dataType = getDataType(this, key);
462 | if (dataType == null) return;
463 | newCbd.set(key, dataType, get(key, dataType));
464 | });
465 | }
466 |
467 | @Override
468 | public void set(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType persistentDataType, final @NotNull Z z) {
469 | pdc.set(namespacedKey, persistentDataType, z);
470 | save();
471 | }
472 |
473 | @Override
474 | public boolean has(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType persistentDataType) {
475 | return pdc.has(namespacedKey, persistentDataType);
476 | }
477 |
478 | @Override
479 | public boolean has(final @NotNull NamespacedKey namespacedKey) {
480 | for (PersistentDataType, ?> type : PRIMITIVE_DATA_TYPES) {
481 | if (pdc.has(namespacedKey, type)) return true;
482 | }
483 | return false;
484 | }
485 |
486 | @Nullable
487 | @Override
488 | public Z get(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType persistentDataType) {
489 | return pdc.get(namespacedKey, persistentDataType);
490 | }
491 |
492 | @NotNull
493 | @Override
494 | public Z getOrDefault(final @NotNull NamespacedKey namespacedKey, final @NotNull PersistentDataType persistentDataType, final @NotNull Z z) {
495 | return pdc.getOrDefault(namespacedKey, persistentDataType, z);
496 | }
497 |
498 | @NotNull
499 | @Override
500 | public Set getKeys() {
501 | return pdc.getKeys();
502 | }
503 |
504 | @Override
505 | public void remove(final @NotNull NamespacedKey namespacedKey) {
506 | pdc.remove(namespacedKey);
507 | save();
508 | }
509 |
510 | @Override
511 | public boolean isEmpty() {
512 | return pdc.isEmpty();
513 | }
514 |
515 | @NotNull
516 | @Override
517 | public PersistentDataAdapterContext getAdapterContext() {
518 | return pdc.getAdapterContext();
519 | }
520 |
521 | /**
522 | * @see PersistentDataContainer#serializeToBytes()
523 | * @deprecated Paper-only
524 | */
525 | @NotNull
526 | @Override
527 | @PaperOnly
528 | @Deprecated
529 | public byte[] serializeToBytes() throws IOException {
530 | return pdc.serializeToBytes();
531 | }
532 |
533 | /**
534 | * @see PersistentDataContainer#readFromBytes(byte[], boolean) ()
535 | * @deprecated Paper-only
536 | */
537 | @Override
538 | @PaperOnly
539 | @Deprecated
540 | public void readFromBytes(byte[] bytes, boolean clear) throws IOException {
541 | pdc.readFromBytes(bytes, clear);
542 | }
543 |
544 | /**
545 | * @see PersistentDataContainer#readFromBytes(byte[]) ()
546 | * @deprecated Paper-only
547 | */
548 | @Override
549 | @PaperOnly
550 | @Deprecated
551 | public void readFromBytes(byte[] bytes) throws IOException {
552 | pdc.readFromBytes(bytes);
553 | }
554 |
555 | /**
556 | * Gets the proper primitive {@link PersistentDataType} for the given {@link NamespacedKey}
557 | *
558 | * @return The primitive PersistentDataType for the given key, or null if the key doesn't exist
559 | */
560 | public PersistentDataType, ?> getDataType(NamespacedKey key) {
561 | return getDataType(this, key);
562 | }
563 |
564 | /**
565 | * Indicates a method that only works on Paper and forks, but not on Spigot or CraftBukkit
566 | */
567 | @Retention(RetentionPolicy.CLASS)
568 | @Target(ElementType.METHOD)
569 | private @interface PaperOnly {
570 | }
571 |
572 | private static final class DataType {
573 | private static final PersistentDataType BOOLEAN = new PersistentDataType() {
574 | @NotNull
575 | @Override
576 | public Class getPrimitiveType() {
577 | return Byte.class;
578 | }
579 |
580 | @NotNull
581 | @Override
582 | public Class getComplexType() {
583 | return Boolean.class;
584 | }
585 |
586 | @NotNull
587 | @Override
588 | public Byte toPrimitive(@NotNull Boolean complex, @NotNull PersistentDataAdapterContext context) {
589 | return complex ? (byte) 1 : (byte) 0;
590 | }
591 |
592 | @NotNull
593 | @Override
594 | public Boolean fromPrimitive(@NotNull Byte primitive, @NotNull PersistentDataAdapterContext context) {
595 | return primitive == (byte) 1;
596 | }
597 | };
598 | }
599 | }
600 |
601 |
--------------------------------------------------------------------------------
/src/main/java/com/jeff_media/customblockdata/events/CustomBlockDataEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Alexander Majka (mfnalex) / JEFF Media GbR
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | *
13 | * If you need help or have any suggestions, feel free to join my Discord and head to #programming-help:
14 | *
15 | * Discord: https://discord.jeff-media.com/
16 | *
17 | * If you find this library helpful or if you're using it one of your paid plugins, please consider leaving a donation
18 | * to support the further development of this project :)
19 | *
20 | * Donations: https://paypal.me/mfnalex
21 | */
22 |
23 | package com.jeff_media.customblockdata.events;
24 |
25 | import com.jeff_media.customblockdata.CustomBlockData;
26 | import org.bukkit.block.Block;
27 | import org.bukkit.event.Cancellable;
28 | import org.bukkit.event.Event;
29 | import org.bukkit.event.HandlerList;
30 | import org.bukkit.event.block.*;
31 | import org.bukkit.event.entity.EntityChangeBlockEvent;
32 | import org.bukkit.event.entity.EntityExplodeEvent;
33 | import org.bukkit.event.world.StructureGrowEvent;
34 | import org.bukkit.plugin.Plugin;
35 | import org.jetbrains.annotations.NotNull;
36 |
37 | import java.util.Arrays;
38 | import java.util.List;
39 |
40 | /**
41 | * Represents an event that removes, changes or moves CustomBlockData due to regular Bukkit Events.
42 | *
43 | * This event gets called during the underlying Bukkit Event's MONITOR listening phase. If the Bukkit Event
44 | * was already cancelled, this event will not be called.
45 | *
46 | * If this event is cancelled, CustomBlockData will not be removed, changed or moved.
47 | */
48 | public class CustomBlockDataEvent extends Event implements Cancellable {
49 |
50 | private static final HandlerList HANDLERS = new HandlerList();
51 |
52 | final @NotNull Plugin plugin;
53 | final @NotNull Block block;
54 | final @NotNull CustomBlockData cbd;
55 | final @NotNull Event bukkitEvent;
56 | boolean isCancelled = false;
57 |
58 | protected CustomBlockDataEvent(@NotNull Plugin plugin, @NotNull Block block, @NotNull Event bukkitEvent) {
59 | this.plugin = plugin;
60 | this.block = block;
61 | this.bukkitEvent = bukkitEvent;
62 | this.cbd = new CustomBlockData(block, plugin);
63 | }
64 |
65 | /**
66 | * Gets the block involved in this event.
67 | */
68 | public @NotNull Block getBlock() {
69 | return block;
70 | }
71 |
72 | /**
73 | * Gets the underlying Bukkit Event that has caused this event to be called. The Bukkit Event is currently listened
74 | * on in MONITOR priority.
75 | */
76 | public @NotNull Event getBukkitEvent() {
77 | return bukkitEvent;
78 | }
79 |
80 | /**
81 | * Gets the CustomBlockData involved in this event.
82 | */
83 | public @NotNull CustomBlockData getCustomBlockData() {
84 | return cbd;
85 | }
86 |
87 | /**
88 | * Gets the cancellation status of this event.
89 | */
90 | @Override
91 | public boolean isCancelled() {
92 | return isCancelled;
93 | }
94 |
95 | /**
96 | * Sets the cancellation status of this event. If the event is cancelled, the CustomBlockData will not be removed, changed or moved.
97 | */
98 | @Override
99 | public void setCancelled(boolean cancel) {
100 | this.isCancelled = cancel;
101 | }
102 |
103 | /**
104 | * Gets the reason for this change of CustomBlockData
105 | */
106 | public @NotNull Reason getReason() {
107 | if (bukkitEvent == null) return Reason.UNKNOWN;
108 | for (Reason reason : Reason.values()) {
109 | if (reason == Reason.UNKNOWN) continue;
110 | if (reason.eventClasses.stream().anyMatch(clazz -> clazz.equals(bukkitEvent.getClass()))) return reason;
111 | }
112 | return Reason.UNKNOWN;
113 | }
114 |
115 | @NotNull
116 | public static HandlerList getHandlerList() {
117 | return HANDLERS;
118 | }
119 |
120 | @NotNull
121 | @Override
122 | public HandlerList getHandlers() {
123 | return HANDLERS;
124 | }
125 |
126 | /**
127 | * Represents the reason for a change of CustomBlockData
128 | */
129 | public enum Reason {
130 | /**
131 | * Represents a block being broken by a player
132 | * @see BlockBreakEvent
133 | */
134 | BLOCK_BREAK(BlockBreakEvent.class),
135 | /**
136 | * Represents a block being replaced by a new block (for example STONE being placed into a TALL_GRASS)
137 | * @see BlockPlaceEvent
138 | * @see BlockMultiPlaceEvent
139 | */
140 | BLOCK_PLACE(BlockPlaceEvent.class, BlockMultiPlaceEvent.class),
141 | /**
142 | * Represents a block being destroyed by an explosion
143 | * @see BlockExplodeEvent
144 | * @see EntityExplodeEvent
145 | */
146 | EXPLOSION(EntityExplodeEvent.class, BlockExplodeEvent.class),
147 | /**
148 | * Represents a block being moved by a piston
149 | * @see CustomBlockDataMoveEvent
150 | * @see BlockPistonExtendEvent
151 | * @see BlockPistonRetractEvent
152 | */
153 | PISTON(BlockPistonExtendEvent.class, BlockPistonRetractEvent.class),
154 | /**
155 | * Represents a block being destroyed by fire
156 | * @see BlockBurnEvent
157 | */
158 | BURN(BlockBurnEvent.class),
159 | /**
160 | * Represents a block being changed by an entity. An {@link EntityChangeBlockEvent} will only trigger removal
161 | * of CustomBlockData when the block's material changes.
162 | *
163 | * Example: When a player steps on REDSTONE_ORE, an EntityChangeBlockEvent is called because the BlockState's
164 | * "lit" tag changes from false to true. However, this will not lead to removal of CustomBlockData because
165 | * the block's material is still REDSTONE_ORE.
166 | *
167 | */
168 | ENTITY_CHANGE_BLOCK(EntityChangeBlockEvent.class),
169 | /**
170 | * Represents a block being destroyed by melting, etc. A {@link BlockFadeEvent} will only trigger
171 | * removal of CustomBlockData when the block's material changes. The event will not be called for fire
172 | * burning out.
173 | * @see BlockFadeEvent
174 | */
175 | FADE(BlockFadeEvent.class),
176 | /**
177 | * Represents a block being changed by a structure (Sapling -> Tree, Mushroom -> Huge Mushroom), naturally or using bonemeal.
178 | * @see StructureGrowEvent
179 | */
180 | STRUCTURE_GROW(StructureGrowEvent.class),
181 | /**
182 | * Represents a block being changed by fertilizing a given block with bonemeal.
183 | * @see BlockFertilizeEvent
184 | */
185 | FERTILIZE(BlockFertilizeEvent.class),
186 | /**
187 | * Represents leaves decaying. This is currently not called because of performance reasons. In future versions,
188 | * there will be a method to enable listening to this.
189 | * @deprecated Draft API
190 | */
191 | @Deprecated
192 | LEAVES_DECAY(LeavesDecayEvent.class),
193 |
194 | UNKNOWN((Class extends Event>) null);
195 |
196 | private final @NotNull List> eventClasses;
197 |
198 | @SafeVarargs
199 | Reason(Class extends Event>... eventClasses) {
200 | this.eventClasses = Arrays.asList(eventClasses);
201 | }
202 |
203 | /**
204 | * Gets a list of Bukkit Event classes that are associated with this Reason
205 | */
206 | public @NotNull List> getApplicableEvents() {
207 | return this.eventClasses;
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/main/java/com/jeff_media/customblockdata/events/CustomBlockDataMoveEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Alexander Majka (mfnalex) / JEFF Media GbR
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | *
13 | * If you need help or have any suggestions, feel free to join my Discord and head to #programming-help:
14 | *
15 | * Discord: https://discord.jeff-media.com/
16 | *
17 | * If you find this library helpful or if you're using it one of your paid plugins, please consider leaving a donation
18 | * to support the further development of this project :)
19 | *
20 | * Donations: https://paypal.me/mfnalex
21 | */
22 |
23 | package com.jeff_media.customblockdata.events;
24 |
25 | import com.jeff_media.customblockdata.CustomBlockData;
26 | import org.bukkit.block.Block;
27 | import org.bukkit.event.Event;
28 | import org.bukkit.event.HandlerList;
29 | import org.bukkit.plugin.Plugin;
30 | import org.jetbrains.annotations.NotNull;
31 |
32 | /**
33 | * Called when a block with CustomBlockData is moved by a piston to a new location.
34 | *
35 | * Blocks with protected CustomBlockData (see {@link CustomBlockData#isProtected()} will not trigger this event, however
36 | * it is possible that unprotected CustomBlockData will be moved to a destination block with protected CustomBlockData. You have
37 | * to cancel this event yourself to prevent this.
38 | */
39 | public class CustomBlockDataMoveEvent extends CustomBlockDataEvent {
40 |
41 | private final @NotNull Block blockTo;
42 |
43 | public CustomBlockDataMoveEvent(@NotNull Plugin plugin, @NotNull Block blockFrom, @NotNull Block blockTo, @NotNull Event bukkitEvent) {
44 | super(plugin, blockFrom, bukkitEvent);
45 | this.blockTo = blockTo;
46 | }
47 |
48 | public @NotNull Block getBlockTo() {
49 | return blockTo;
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/com/jeff_media/customblockdata/events/CustomBlockDataRemoveEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Alexander Majka (mfnalex) / JEFF Media GbR
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | *
13 | * If you need help or have any suggestions, feel free to join my Discord and head to #programming-help:
14 | *
15 | * Discord: https://discord.jeff-media.com/
16 | *
17 | * If you find this library helpful or if you're using it one of your paid plugins, please consider leaving a donation
18 | * to support the further development of this project :)
19 | *
20 | * Donations: https://paypal.me/mfnalex
21 | */
22 |
23 | package com.jeff_media.customblockdata.events;
24 |
25 | import org.bukkit.block.Block;
26 | import org.bukkit.event.Event;
27 | import org.bukkit.event.HandlerList;
28 | import org.bukkit.plugin.Plugin;
29 | import org.jetbrains.annotations.NotNull;
30 | import org.jetbrains.annotations.Nullable;
31 |
32 | /**
33 | * Called when a block's CustomBlockData is about to be removed because the block was broken, replaced, or has changed in other ways.
34 | */
35 | public class CustomBlockDataRemoveEvent extends CustomBlockDataEvent {
36 |
37 | public CustomBlockDataRemoveEvent(@NotNull Plugin plugin, @NotNull Block block, @Nullable Event bukkitEvent) {
38 | super(plugin, block, bukkitEvent);
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/jeff_media/customblockdata/events/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Alexander Majka (mfnalex) / JEFF Media GbR
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | *
13 | * If you need help or have any suggestions, feel free to join my Discord and head to #programming-help:
14 | *
15 | * Discord: https://discord.jeff-media.com/
16 | *
17 | * If you find this library helpful or if you're using it one of your paid plugins, please consider leaving a donation
18 | * to support the further development of this project :)
19 | *
20 | * Donations: https://paypal.me/mfnalex
21 | */
22 | /**
23 | * Classes dedicated to handling triggered code executions
24 | */
25 | package com.jeff_media.customblockdata.events;
--------------------------------------------------------------------------------
/src/main/java/com/jeff_media/customblockdata/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Alexander Majka (mfnalex) / JEFF Media GbR
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5 | * the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11 | * specific language governing permissions and limitations under the License.
12 | *
13 | * If you need help or have any suggestions, feel free to join my Discord and head to #programming-help:
14 | *
15 | * Discord: https://discord.jeff-media.com/
16 | *
17 | * If you find this library helpful or if you're using it one of your paid plugins, please consider leaving a donation
18 | * to support the further development of this project :)
19 | *
20 | * Donations: https://paypal.me/mfnalex
21 | */
22 | /**
23 | * The root package of the CustomBlockData API
24 | */
25 | package com.jeff_media.customblockdata;
--------------------------------------------------------------------------------
/src/main/javadoc/overview.html:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 | CustomBlockData is a library for the Bukkit API to provide a {@link org.bukkit.persistence.PersistentDataContainer}
26 | for every Block in the world.
27 |
28 |
29 | It does not need any files or databases. All data is stored inside the chunk's PersistentDataContainer and is
30 | persistent after server restart. CustomBlockData can also automatically delete your custom data when a block
31 | gets broken, move your data if the block gets moved by pistons, etc. By default, however, and for backward compatibility reasons, data stored inside blocks is independent of the underlying block.
32 | That means: if you store some data inside a dirt block, and that block is now pushed by a piston, then the information
33 | will still reside in the old block's location, unless you have enabled this feature explicitly using {@link com.jeff_media.customblockdata.CustomBlockData#registerListener(Plugin)}.
34 |
35 |
36 | There are a few things you should keep in mind when using this library:
37 |
38 |
39 | -
40 | All data you add to a block gets saved into the {@link org.bukkit.Chunk}'s PersistentDataContainer, which in
41 | return is saved directly into your {@link org.bukkit.World}'s region files.
42 |
43 | -
44 | This library is not intended to store excessive amounts of data. With "excessive amounts", I am
45 | talking about tens or hundreds of Megabytes per Chunk.
46 |
47 |
48 | Need more PersistentDataTypes?
49 |
50 | If you feel limited by the existing PersistentDataTypes, take a look at my MorePersistentDataTypes library.
51 | It doesn't only add more than 40 new PersistentDataTypes, but also allows to you to store any kind of Collection, Map or Array
52 | inside the PersistentDataContainer. Of course it also supports nested Maps and Collections like for example
53 | Map<String<Map<Integer,ItemStack>>. You can find that library on GitHub
54 | and on SpigotMC.
55 |
56 |
--------------------------------------------------------------------------------