diff --git a/addon/README.md b/addon/README.md
new file mode 100644
index 00000000..b61a33d3
--- /dev/null
+++ b/addon/README.md
@@ -0,0 +1,7 @@
+## DungeonsXL Donors Addon
+
+(C) 2020 Daniel Saukel, All Rights Reserved.
+
+This module is a plugin with additional features made for donors.
+
+The GNU LGPLv3 of the API and the GNU GPLv3 license of the other modules do not apply to this module.
diff --git a/addon/core/pom.xml b/addon/core/pom.xml
new file mode 100644
index 00000000..d42464a8
--- /dev/null
+++ b/addon/core/pom.xml
@@ -0,0 +1,32 @@
+
+ 4.0.0
+ de.erethon.dungeonsxl
+ dungeonsxl-addon-core
+ ${project.parent.version}
+ jar
+
+ de.erethon.dungeonsxl
+ dungeonsxl-addon
+ 0.0.1-SNAPSHOT
+
+
+
+
+ .
+ true
+ src/main/resources/
+
+ plugin.yml
+
+
+
+
+
+
+ org.spigotmc
+ spigot
+ ${spigotVersion.latest}
+ provided
+
+
+
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/DungeonsXXL.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/DungeonsXXL.java
new file mode 100644
index 00000000..1e94e9cb
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/DungeonsXXL.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl;
+
+import de.erethon.commons.compatibility.Internals;
+import de.erethon.commons.javaplugin.DREPlugin;
+import de.erethon.commons.javaplugin.DREPluginSettings;
+import de.erethon.dungeonsxl.DungeonsXL;
+import de.erethon.dungeonsxxl.requirement.*;
+import de.erethon.dungeonsxxl.sign.*;
+import de.erethon.dungeonsxxl.util.GlowUtil;
+
+/**
+ * @author Daniel Saukel
+ */
+public class DungeonsXXL extends DREPlugin {
+
+ private DungeonsXL dxl;
+ private GlowUtil glowUtil;
+
+ public DungeonsXXL() {
+ settings = DREPluginSettings.builder()
+ .internals(Internals.v1_15_R1)
+ .metrics(false)
+ .spigotMCResourceId(-1)
+ .build();
+ }
+
+ @Override
+ public void onEnable() {
+ dxl = DungeonsXL.getInstance();
+ glowUtil = new GlowUtil(this);
+
+ dxl.getRequirementRegistry().add("feeItems", FeeItemsRequirement.class);
+
+ dxl.getSignRegistry().add("Firework", FireworkSign.class);
+ dxl.getSignRegistry().add("GlowingBlock", GlowingBlockSign.class);
+ dxl.getSignRegistry().add("InteractWall", InteractWallSign.class);
+ dxl.getSignRegistry().add("Particle", ParticleSign.class);
+ }
+
+ /**
+ * Returns the instance of this plugin.
+ *
+ * @return the instance of this plugin
+ */
+ public static DungeonsXXL getInstance() {
+ return (DungeonsXXL) DREPlugin.getInstance();
+ }
+
+ /**
+ * Returns the current {@link de.erethon.dungeonsxl.DungeonsXL} singleton.
+ *
+ * @return the current {@link de.erethon.dungeonsxl.DungeonsXL} singleton
+ */
+ public DungeonsXL getDXL() {
+ return dxl;
+ }
+
+ /**
+ * The loaded instance of GlowUtil.
+ *
+ * @return the loaded instance of GlowUtil
+ */
+ public GlowUtil getGlowUtil() {
+ return glowUtil;
+ }
+
+}
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/requirement/FeeItemsRequirement.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/requirement/FeeItemsRequirement.java
new file mode 100644
index 00000000..aabdc8f9
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/requirement/FeeItemsRequirement.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl.requirement;
+
+import de.erethon.dungeonsxl.api.DungeonsAPI;
+import de.erethon.dungeonsxl.api.Requirement;
+import de.erethon.dungeonsxl.config.DMessage;
+import java.util.List;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * @author Daniel Saukel
+ */
+public class FeeItemsRequirement implements Requirement {
+
+ private DungeonsAPI api;
+
+ private List fee;
+
+ public FeeItemsRequirement(DungeonsAPI api) {
+ this.api = api;
+ }
+
+ public List getFee() {
+ return fee;
+ }
+
+ @Override
+ public void setup(ConfigurationSection config) {
+ fee = api.getCaliburn().deserializeStackList(config, "feeItems");
+ }
+
+ @Override
+ public boolean check(Player player) {
+ for (ItemStack stack : fee) {
+ if (!player.getInventory().containsAtLeast(stack, stack.getAmount())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseComponent[] getCheckMessage(Player player) {
+ ComponentBuilder builder = new ComponentBuilder(DMessage.REQUIREMENT_FEE_ITEMS + ": ").color(ChatColor.GOLD);
+ boolean first = true;
+ for (ItemStack stack : fee) {
+ String name = stack.getAmount() > 1 ? stack.getAmount() + " " : "" + api.getCaliburn().getExItem(stack).getName();
+ ChatColor color = player.getInventory().containsAtLeast(stack, stack.getAmount()) ? ChatColor.GREEN : ChatColor.DARK_RED;
+ if (!first) {
+ builder.append(", ").color(ChatColor.WHITE);
+ } else {
+ first = false;
+ }
+ builder.append(name).color(color);
+ }
+ return builder.create();
+ }
+
+ @Override
+ public void demand(Player player) {
+ player.getInventory().removeItem(fee.toArray(new ItemStack[]{}));
+ }
+
+ @Override
+ public String toString() {
+ return "FeeItemsRequirement{items=" + fee + "}";
+ }
+
+}
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/FireworkSign.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/FireworkSign.java
new file mode 100644
index 00000000..ea5e6766
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/FireworkSign.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl.sign;
+
+import de.erethon.dungeonsxl.api.DungeonsAPI;
+import de.erethon.dungeonsxl.api.sign.Button;
+import de.erethon.dungeonsxl.api.world.InstanceWorld;
+import de.erethon.dungeonsxl.player.DPermission;
+import de.erethon.dungeonsxxl.util.FireworkUtil;
+import org.bukkit.block.Sign;
+
+/**
+ * @author Daniel Saukel
+ */
+public class FireworkSign extends Button {
+
+ public FireworkSign(DungeonsAPI api, Sign sign, String[] lines, InstanceWorld instance) {
+ super(api, sign, lines, instance);
+ }
+
+ @Override
+ public String getName() {
+ return "Firework";
+ }
+
+ @Override
+ public String getBuildPermission() {
+ return DPermission.SIGN.getNode() + ".firework";
+ }
+
+ @Override
+ public boolean isOnDungeonInit() {
+ return false;
+ }
+
+ @Override
+ public boolean isProtected() {
+ return false;
+ }
+
+ @Override
+ public boolean isSetToAir() {
+ return true;
+ }
+
+ @Override
+ public boolean validate() {
+ return true;
+ }
+
+ @Override
+ public void initialize() {
+ }
+
+ @Override
+ public void push() {
+ FireworkUtil.spawnRandom(getSign().getLocation());
+ }
+
+}
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/GlowingBlockSign.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/GlowingBlockSign.java
new file mode 100644
index 00000000..bf612545
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/GlowingBlockSign.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl.sign;
+
+import de.erethon.commons.misc.BlockUtil;
+import de.erethon.commons.misc.EnumUtil;
+import de.erethon.dungeonsxl.api.DungeonsAPI;
+import de.erethon.dungeonsxl.api.sign.Rocker;
+import de.erethon.dungeonsxl.api.world.InstanceWorld;
+import de.erethon.dungeonsxl.player.DPermission;
+import de.erethon.dungeonsxl.world.DGameWorld;
+import de.erethon.dungeonsxxl.DungeonsXXL;
+import de.erethon.dungeonsxxl.world.block.GlowingBlock;
+import org.bukkit.ChatColor;
+import org.bukkit.block.Sign;
+
+/**
+ * Turns the attached block into a glowing block.
+ *
+ * @author Daniel Saukel
+ */
+public class GlowingBlockSign extends Rocker {
+
+ private ChatColor color = ChatColor.DARK_RED;
+ private Double time;
+
+ private GlowingBlock glowingBlock;
+
+ public GlowingBlockSign(DungeonsAPI api, Sign sign, String[] lines, InstanceWorld instance) {
+ super(api, sign, lines, instance);
+ }
+
+ /**
+ * Returns the glowing block.
+ *
+ * @return the glowing block
+ */
+ public GlowingBlock getGlowingBlock() {
+ return glowingBlock;
+ }
+
+ /**
+ * Returns the color of the glowing block or null if it is a rainbow block.
+ *
+ * @return the color of the glowing block or null if it is a rainbow block
+ */
+ public ChatColor getColor() {
+ return color;
+ }
+
+ @Override
+ public String getName() {
+ return "GlowingBlock";
+ }
+
+ @Override
+ public String getBuildPermission() {
+ return DPermission.SIGN.getNode() + ".glowingblock";
+ }
+
+ @Override
+ public boolean isOnDungeonInit() {
+ return false;
+ }
+
+ @Override
+ public boolean isProtected() {
+ return false;
+ }
+
+ @Override
+ public boolean isSetToAir() {
+ return true;
+ }
+
+ @Override
+ public boolean validate() {
+ return true;
+ }
+
+ @Override
+ public void initialize() {
+ if (getLine(1).equalsIgnoreCase("RAINBOW")) {
+ color = null;
+ } else {
+ ChatColor color = EnumUtil.getEnumIgnoreCase(ChatColor.class, getLine(1));
+ if (color != null) {
+ this.color = color;
+ }
+ }
+ try {
+ time = Double.parseDouble(getLine(2));
+ } catch (NumberFormatException exception) {
+ }
+ }
+
+ @Override
+ public void activate() {
+ if (active) {
+ return;
+ }
+
+ ((DGameWorld) getGameWorld()).addGameBlock(
+ glowingBlock = new GlowingBlock(DungeonsXXL.getInstance(), BlockUtil.getAttachedBlock(getSign().getBlock()), color, time));
+ active = true;
+ }
+
+ @Override
+ public void deactivate() {
+ if (!active) {
+ return;
+ }
+
+ glowingBlock.removeGlow();
+ active = false;
+ }
+
+}
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/InteractWallSign.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/InteractWallSign.java
new file mode 100644
index 00000000..12d952e9
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/InteractWallSign.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl.sign;
+
+import de.erethon.commons.misc.BlockUtil;
+import de.erethon.commons.misc.NumberUtil;
+import de.erethon.dungeonsxl.api.DungeonsAPI;
+import de.erethon.dungeonsxl.api.world.InstanceWorld;
+import de.erethon.dungeonsxl.player.DPermission;
+import de.erethon.dungeonsxl.sign.passive.InteractSign;
+import de.erethon.dungeonsxl.trigger.InteractTrigger;
+import de.erethon.dungeonsxl.world.DGameWorld;
+import org.bukkit.block.Sign;
+
+/**
+ * This sign adds an interact trigger to an attached block, like a "suspicious wall".
+ *
+ * @author Daniel Saukel
+ */
+public class InteractWallSign extends InteractSign {
+
+ public InteractWallSign(DungeonsAPI api, Sign sign, String[] lines, InstanceWorld instance) {
+ super(api, sign, lines, instance);
+ }
+
+ @Override
+ public String getName() {
+ return "InteractWall";
+ }
+
+ @Override
+ public String getBuildPermission() {
+ return DPermission.SIGN.getNode() + ".interactwall";
+ }
+
+ @Override
+ public boolean isOnDungeonInit() {
+ return false;
+ }
+
+ @Override
+ public boolean isProtected() {
+ return true;
+ }
+
+ @Override
+ public boolean isSetToAir() {
+ return true;
+ }
+
+ @Override
+ public void initialize() {
+ InteractTrigger trigger = InteractTrigger.getOrCreate(NumberUtil.parseInt(getSign().getLine(1)),
+ BlockUtil.getAttachedBlock(getSign().getBlock()), (DGameWorld) getGameWorld());
+ if (trigger != null) {
+ trigger.addListener(this);
+ addTrigger(trigger);
+ }
+ }
+
+}
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/ParticleSign.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/ParticleSign.java
new file mode 100644
index 00000000..744bc536
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/sign/ParticleSign.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl.sign;
+
+import de.erethon.commons.misc.EnumUtil;
+import de.erethon.commons.misc.NumberUtil;
+import de.erethon.dungeonsxl.api.DungeonsAPI;
+import de.erethon.dungeonsxl.api.sign.Button;
+import de.erethon.dungeonsxl.api.world.InstanceWorld;
+import de.erethon.dungeonsxl.player.DPermission;
+import org.bukkit.Particle;
+import org.bukkit.block.Sign;
+
+/**
+ * Spawns particles.
+ *
+ * @author Daniel Saukel
+ */
+public class ParticleSign extends Button {
+
+ private Particle particle;
+ private int count;
+ private double offsetX, offsetY, offsetZ;
+ private double extra = 1;
+
+ public ParticleSign(DungeonsAPI api, Sign sign, String[] lines, InstanceWorld instance) {
+ super(api, sign, lines, instance);
+ }
+
+ @Override
+ public String getName() {
+ return "Particle";
+ }
+
+ @Override
+ public String getBuildPermission() {
+ return DPermission.SIGN.getNode() + ".particle";
+ }
+
+ @Override
+ public boolean isOnDungeonInit() {
+ return false;
+ }
+
+ @Override
+ public boolean isProtected() {
+ return false;
+ }
+
+ @Override
+ public boolean isSetToAir() {
+ return true;
+ }
+
+ @Override
+ public boolean validate() {
+ particle = EnumUtil.getEnumIgnoreCase(Particle.class, getLine(1));
+ if (particle == null) {
+ markAsErroneous("Unknown particle type: " + getLine(1));
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void initialize() {
+ String[] args = getLine(2).split(",");
+ if (args.length == 1) {
+ extra = NumberUtil.parseDouble(args[0], 1);
+ } else if (args.length >= 3) {
+ offsetX = NumberUtil.parseDouble(args[0], 0);
+ offsetX = NumberUtil.parseDouble(args[1], 0);
+ offsetX = NumberUtil.parseDouble(args[2], 0);
+ if (args.length == 4) {
+ extra = NumberUtil.parseDouble(args[3], 1);
+ }
+ }
+ }
+
+ @Override
+ public void push() {
+ getSign().getWorld().spawnParticle(particle, getSign().getLocation(), count, offsetX, offsetY, offsetZ, extra);
+ }
+
+}
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/util/FireworkUtil.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/util/FireworkUtil.java
new file mode 100644
index 00000000..15438cb9
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/util/FireworkUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl.util;
+
+import java.util.Random;
+import org.bukkit.Color;
+import static org.bukkit.Color.*;
+import org.bukkit.FireworkEffect;
+import org.bukkit.Location;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Firework;
+import org.bukkit.inventory.meta.FireworkMeta;
+
+/**
+ * Util class for randomized fireworks.
+ *
+ * @author Daniel Saukel
+ */
+public class FireworkUtil {
+
+ private static final Random RANDOM = new Random();
+ private static final Color[] COLORS = {YELLOW, AQUA, BLACK, BLUE, FUCHSIA, GRAY, GREEN, LIME, MAROON, NAVY, OLIVE, ORANGE, PURPLE, RED, SILVER, TEAL, WHITE};
+
+ /**
+ * Spawns a randomized firework.
+ *
+ * @param location the location where the firework is fired
+ * @return the Firework
+ */
+ public static Firework spawnRandom(Location location) {
+ Firework firework = (Firework) location.getWorld().spawnEntity(location, EntityType.FIREWORK);
+ FireworkMeta meta = firework.getFireworkMeta();
+ Random r = new Random();
+ int rt = r.nextInt(4) + 1;
+ FireworkEffect.Type type = FireworkEffect.Type.BALL;
+ if (rt == 1) {
+ type = FireworkEffect.Type.BALL;
+ }
+ if (rt == 2) {
+ type = FireworkEffect.Type.BALL_LARGE;
+ }
+ if (rt == 3) {
+ type = FireworkEffect.Type.BURST;
+ }
+ if (rt == 4) {
+ type = FireworkEffect.Type.CREEPER;
+ }
+ if (rt == 5) {
+ type = FireworkEffect.Type.STAR;
+ }
+ FireworkEffect effect = FireworkEffect.builder().flicker(r.nextBoolean()).withColor(randomColor()).withFade(randomColor()).with(type).trail(r.nextBoolean()).build();
+ meta.addEffect(effect);
+ int rp = r.nextInt(2) + 1;
+ meta.setPower(rp);
+ firework.setFireworkMeta(meta);
+ return firework;
+ }
+
+ private static Color randomColor() {
+ return COLORS[RANDOM.nextInt(COLORS.length - 1)];
+ }
+
+}
diff --git a/addon/core/src/main/java/de/erethon/dungeonsxxl/util/GlowUtil.java b/addon/core/src/main/java/de/erethon/dungeonsxxl/util/GlowUtil.java
new file mode 100644
index 00000000..efd62a2b
--- /dev/null
+++ b/addon/core/src/main/java/de/erethon/dungeonsxxl/util/GlowUtil.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2020 Daniel Saukel
+ *
+ * All rights reserved.
+ */
+package de.erethon.dungeonsxxl.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import net.minecraft.server.v1_15_R1.EntityShulker;
+import net.minecraft.server.v1_15_R1.EntityTypes;
+import net.minecraft.server.v1_15_R1.Packet;
+import net.minecraft.server.v1_15_R1.PacketPlayOutEntityDestroy;
+import net.minecraft.server.v1_15_R1.PacketPlayOutSpawnEntityLiving;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.Location;
+import org.bukkit.block.Block;
+import org.bukkit.craftbukkit.v1_15_R1.CraftWorld;
+import org.bukkit.craftbukkit.v1_15_R1.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Shulker;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import org.bukkit.scheduler.BukkitRunnable;
+import org.bukkit.scoreboard.Team;
+
+/**
+ * @author Daniel Saukel
+ */
+public class GlowUtil implements Listener {
+
+ private static final Random RANDOM = new Random();
+
+ private Map teams = new HashMap<>();
+ private GlowData glowingBlocks = new GlowData<>();
+ private Map> playerGlows = new HashMap<>();
+ private GlowRunnable runnable = new GlowRunnable();
+
+ public GlowUtil(Plugin plugin) {
+ runnable.runTaskTimer(plugin, 0L, 2L);
+ Bukkit.getPluginManager().registerEvents(this, plugin);
+ }
+
+ private Team getTeam(ChatColor color) {
+ if (!teams.containsKey(color)) {
+ Team team = Bukkit.getScoreboardManager().getMainScoreboard().getTeam("DXL_" + color.getChar());
+ if (team == null) {
+ team = Bukkit.getScoreboardManager().getMainScoreboard().registerNewTeam("DXL_" + color.getChar());
+ team.setColor(color);
+ }
+ teams.put(color, team);
+ }
+ return teams.get(color);
+ }
+
+ /**
+ * Adds a colored glow effect to the block that is visible to all players.
+ *
+ * @param block the block
+ * @param color the glow color
+ * @return the spawned entity that provides the glow effect
+ */
+ public org.bukkit.entity.Entity addBlockGlow(Block block, ChatColor color) {
+ Shulker entity = block.getWorld().spawn(new Location(block.getWorld(), block.getX() + .5, block.getY(), block.getZ() + .5), Shulker.class);
+ entity.setAI(false);
+ entity.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 0), true);
+ entity.setInvulnerable(true);
+ addGlow(entity, color);
+ glowingBlocks.put(block, entity);
+ return entity;
+ }
+
+ /**
+ * Adds a packet-level colored glow effect to the block that is only visible to certain players.
+ *
+ * @param block the block
+ * @param color the glow color
+ * @param players the players who can see the effect
+ */
+ public void addBlockGlow(Block block, ChatColor color, Player... players) {
+ EntityShulker entity = new EntityShulker(EntityTypes.SHULKER, ((CraftWorld) block.getWorld()).getHandle());
+ entity.setLocation(block.getX() + .5, block.getY(), block.getZ() + .5, 0, 0);
+ entity.setFlag(6, true);
+ entity.setInvisible(true);
+ for (Player player : players) {
+ sendPacket(player, new PacketPlayOutSpawnEntityLiving(entity));
+ if (playerGlows.get(player) == null) {
+ playerGlows.put(player, new GlowData<>());
+ }
+ playerGlows.get(player).put(block, entity);
+ }
+ }
+
+ /**
+ * Adds a rainbow colored glow effect to the block that is visible to all players.
+ *
+ * @param block the block
+ * @return the spawned entity that provides the glow effect
+ */
+ public org.bukkit.entity.Entity addRainbowBlockGlow(Block block) {
+ return addRainbowBlockGlow(block, (Long) null);
+ }
+
+ /**
+ * Adds a rainbow colored glow effect to the block that is visible to all players.
+ *
+ * The task is cancelled automatically when the entity dies.
+ *
+ * @param block the block
+ * @param cancelTime the time in milliseconds until the glow effect shall end; null = forever
+ * @return the spawned entity that provides the glow effect
+ */
+ public org.bukkit.entity.Entity addRainbowBlockGlow(Block block, Long cancelTime) {
+ Shulker entity = block.getWorld().spawn(new Location(block.getWorld(), block.getX() + .5, block.getY(), block.getZ() + .5), Shulker.class);
+ entity.setAI(false);
+ entity.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 0), true);
+ entity.setInvulnerable(true);
+ glowingBlocks.put(block, entity);
+ addRainbowGlow(entity, cancelTime);
+ return entity;
+ }
+
+ /**
+ * Adds a packet-level rainbow colored glow effect to the block that is only visible to certain players.
+ *
+ * Returns the repeating task that handles color changes.
+ *
+ * @param block the block
+ * @param players
+ */
+ public void addRainbowBlockGlow(Block block, Player... players) {
+ addRainbowBlockGlow(block, null, players);
+ }
+
+ /**
+ * Adds a packet-level rainbow colored glow effect to the block that is only visible to certain players.
+ *
+ * Returns the repeating task that handles color changes.
+ *
+ * @param block the block
+ * @param cancelTime the time in milliseconds until the glow effect shall end; null = forever
+ * @param players
+ */
+ public void addRainbowBlockGlow(Block block, Long cancelTime, Player... players) {
+ EntityShulker entity = new EntityShulker(EntityTypes.SHULKER, ((CraftWorld) block.getWorld()).getHandle());
+ entity.setLocation(block.getX() + .5, block.getY(), block.getZ() + .5, 0, 0);
+ entity.setFlag(6, true);
+ entity.setInvisible(true);
+ for (Player player : players) {
+ sendPacket(player, new PacketPlayOutSpawnEntityLiving(entity));
+ if (playerGlows.get(player) == null) {
+ playerGlows.put(player, new GlowData<>());
+ }
+ playerGlows.get(player).put(block, entity);
+ }
+ addRainbowGlow(entity, cancelTime);
+ }
+
+ /**
+ * Removes the glow effect from a glowing block.
+ *
+ * @param block the block
+ */
+ public void removeBlockGlow(Block block) {
+ org.bukkit.entity.Entity bukkitEntity = glowingBlocks.get(block);
+ if (bukkitEntity != null) {
+ bukkitEntity.remove();
+ glowingBlocks.remove(block);
+ runnable.removeEntity(bukkitEntity);
+ }
+
+ for (Entry> entry : playerGlows.entrySet()) {
+ net.minecraft.server.v1_15_R1.Entity nmsEntity = entry.getValue().get(block);
+ if (nmsEntity != null) {
+ sendPacket(entry.getKey(), new PacketPlayOutEntityDestroy(nmsEntity.getId()));
+ runnable.removeEntity(nmsEntity);
+ }
+ }
+ }
+
+ /**
+ * Adds a colored glow effect to an entity and handles its scoreboard team membership.
+ *
+ * @param entity a Bukkit Entity
+ * @param color the glow color
+ */
+ public void addGlow(org.bukkit.entity.Entity entity, ChatColor color) {
+ getTeam(color).addEntry(asEntry(entity));
+ entity.setGlowing(true);
+ }
+
+ /**
+ * Adds a colored glow effect to an entity and handles its scoreboard team membership.
+ *
+ * @param entity an NMS Entity
+ * @param color the glow color
+ */
+ public void addGlow(net.minecraft.server.v1_15_R1.Entity entity, ChatColor color) {
+ getTeam(color).addEntry(asEntry(entity));
+ entity.setFlag(6, true);
+ }
+
+ /**
+ * Adds a changing glow effect to an entity.
+ *
+ * @param entity an NMS Entity
+ */
+ public void addRainbowGlow(org.bukkit.entity.Entity entity) {
+ addRainbowGlow(entity, null);
+ }
+
+ /**
+ * Adds a changing glow effect to an entity.
+ *
+ * @param entity an NMS Entity
+ * @param cancelTime the time in milliseconds until the glow effect shall end; null = forever
+ */
+ public void addRainbowGlow(org.bukkit.entity.Entity entity, Long cancelTime) {
+ entity.setGlowing(true);
+ runnable.addEntity(entity, cancelTime != null ? System.currentTimeMillis() + cancelTime : null);
+ }
+
+ /**
+ * Adds a changing glow effect to an entity.
+ *
+ * @param entity an NMS Entity
+ */
+ public void addRainbowGlow(net.minecraft.server.v1_15_R1.Entity entity) {
+ addRainbowGlow(entity, null);
+ }
+
+ /**
+ * Adds a changing glow effect to an entity.
+ *
+ * @param entity an NMS Entity
+ * @param cancelTime the time in milliseconds until the glow effect shall end; null = forever
+ */
+ public void addRainbowGlow(net.minecraft.server.v1_15_R1.Entity entity, Long cancelTime) {
+ entity.setFlag(6, true);
+ runnable.addEntity(entity, cancelTime != null ? System.currentTimeMillis() + cancelTime : null);
+ }
+
+ /**
+ * Removes the glow effect from an entity and handles its scoreboard team membership.
+ *
+ * @param entity a Bukkit Entity
+ */
+ public void removeGlow(org.bukkit.entity.Entity entity) {
+ entity.setGlowing(false);
+ teams.values().forEach(t -> t.removeEntry(asEntry(entity)));
+ runnable.removeEntity(entity);
+ }
+
+ /**
+ * Removes the glow effect from an entity and handles its scoreboard team membership.
+ *
+ * @param entity an NMS Entity
+ */
+ public void removeGlow(net.minecraft.server.v1_15_R1.Entity entity) {
+ entity.setFlag(6, false);
+ teams.values().forEach(t -> t.removeEntry(asEntry(entity)));
+ runnable.removeEntity(entity);
+ }
+
+ private static String asEntry(org.bukkit.entity.Entity entity) {
+ return entity instanceof Player ? entity.getName() : entity.getUniqueId().toString();
+ }
+
+ private static String asEntry(net.minecraft.server.v1_15_R1.Entity entity) {
+ return entity instanceof Player ? entity.getName() : entity.getUniqueID().toString();
+ }
+
+ private static void sendPacket(Player player, Packet> packet) {
+ ((CraftPlayer) player).getHandle().playerConnection.sendPacket(packet);
+ }
+
+ @EventHandler
+ public void onBlockBreak(BlockBreakEvent event) {
+ removeBlockGlow(event.getBlock());
+ }
+
+ @EventHandler
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ playerGlows.remove(event.getPlayer());
+ }
+
+ private class GlowRunnable extends BukkitRunnable {
+
+ private Map