diff --git a/README.md b/README.md index e3f757b..eae6376 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,14 @@ Note that commands have some restrictions in place, so giving the weakest permis - All sign changes are run through the SignChangeEvent, so the player cannot edit a sign if any protection plugin would prevent them from editing the sign normally. -| Command | Arguments | Description | -|---------------|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| /setSignLine | \ \ \ ... | Sets the text of the sign line (1-4) to the given input. Then right-click the sign to update. | -| /viewSign | \[raw true/false] \[placeholders true/false] | Shows the full contents and details of the sign you are currently looking at. If "raw" is true, formatting codes are displayed. If placeholders is true, stored placeholders are displayed. | -| /copySign | | Copies the sign you are currently looking at to another sign, including placeholders, formatting codes, dye and waxed state. | -| /copySignText | \[side] \[source line] \[target line] | Copies the text of the sign you are currently looking at to another sign, including placeholders and formatting codes. If no argument is given, both sides are copied, and the front text is copies to the side of the target sign you click. If a side is specified, the text of that side is copied, and pasted to the sign side you click (use `this` as the sign side to select the side you are looking at when executing the command). The source line argument is used to only copy a single line from the selected sign side. The target line argument is used to specify the line to paste to (defaults to same as the source line) on the clicked side. | -| /unWaxSign | | Removes the wax from the sign you are currently looking at. | +| Command | Arguments | Description | +|----------------------------|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| /setSignLine | \ \ \ ... | Sets the text of the sign line (1-4) to the given input. Then right-click the sign to update. | +| /viewSign | \[raw true/false] \[placeholders true/false] | Shows the full contents and details of the sign you are currently looking at. If "raw" is true, formatting codes are displayed. If placeholders is true, stored placeholders are displayed. | +| /copySign | | Copies the sign you are currently looking at to another sign, including placeholders, formatting codes, dye and waxed state. | +| /copySignText | \[side] \[source line] \[target line] | Copies the text of the sign you are currently looking at to another sign, including placeholders and formatting codes. If no argument is given, both sides are copied, and the front text is copies to the side of the target sign you click. If a side is specified, the text of that side is copied, and pasted to the sign side you click (use `this` as the sign side to select the side you are looking at when executing the command). The source line argument is used to only copy a single line from the selected sign side. The target line argument is used to specify the line to paste to (defaults to same as the source line) on the clicked side. | +| /unWaxSign | | Removes the wax from the sign you are currently looking at. | +| /setPlaceholderUpdateDelay | \ | Sets the update delay in ticks for the placeholder sign you are currently looking at. Use "null" to un-set the value. 1 second = 20 ticks. | ## Permissions @@ -43,4 +44,5 @@ Note that commands have some restrictions in place, so giving the weakest permis | placeholdersigns.copy | false | Allows unrestricted use of the /copySign and /copySignText commands. | | placeholdersigns.copy.use | false | Allows use of the /copySign and /copySignText commands. | | placeholdersigns.copy.bypass-waxed | false | Allows pasting a sign copied with /copySign and /copySignText onto a waxed sign. | -| placeholdersigns.unwax | false | Allows use of the /unWaxSign command | \ No newline at end of file +| placeholdersigns.unwax | false | Allows use of the /unWaxSign command | +| placeholdersigns.setdelay | false | Allows use of the /setPlaceholderUpdateDelay command | \ No newline at end of file diff --git a/src/main/java/net/knarcraft/placeholdersigns/PlaceholderSigns.java b/src/main/java/net/knarcraft/placeholdersigns/PlaceholderSigns.java index f1e84d3..4a0e347 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/PlaceholderSigns.java +++ b/src/main/java/net/knarcraft/placeholdersigns/PlaceholderSigns.java @@ -6,11 +6,14 @@ import net.knarcraft.knarlib.property.ColorConversion; import net.knarcraft.placeholdersigns.command.CopySignCommand; import net.knarcraft.placeholdersigns.command.CopySignTextCommand; import net.knarcraft.placeholdersigns.command.EditSignCommand; +import net.knarcraft.placeholdersigns.command.SetPlaceholderUpdateDelayCommand; import net.knarcraft.placeholdersigns.command.UnWaxSignCommand; import net.knarcraft.placeholdersigns.command.ViewSignCommand; import net.knarcraft.placeholdersigns.config.PlaceholderSignMessage; import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler; import net.knarcraft.placeholdersigns.handler.PlaceholderSignRequestHandler; +import net.knarcraft.placeholdersigns.handler.PlaceholderSignUpdateQueueHandler; +import net.knarcraft.placeholdersigns.listener.ChunkListener; import net.knarcraft.placeholdersigns.listener.SignBreakListener; import net.knarcraft.placeholdersigns.listener.SignClickListener; import net.knarcraft.placeholdersigns.listener.SignTextListener; @@ -18,6 +21,7 @@ import net.knarcraft.placeholdersigns.runnable.SignUpdate; import org.bukkit.Bukkit; import org.bukkit.command.CommandExecutor; import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; @@ -31,7 +35,9 @@ public final class PlaceholderSigns extends JavaPlugin { private static PlaceholderSigns instance; private PlaceholderSignHandler signHandler; private PlaceholderSignRequestHandler requestHandler; + private PlaceholderSignUpdateQueueHandler updateQueueHandler; private StringFormatter stringFormatter; + private int signUpdateDelay; /** * Gets an instance of this plugin @@ -53,21 +59,51 @@ public final class PlaceholderSigns extends JavaPlugin { return this.signHandler; } + /** + * Gets this instance's placeholder sign request handler + * + * @return

The request handler

+ */ @NotNull public PlaceholderSignRequestHandler getRequestHandler() { return this.requestHandler; } + /** + * Gets this instance's update queue handler + * + * @return

The update queue handler

+ */ + @NotNull + public PlaceholderSignUpdateQueueHandler getUpdateQueueHandler() { + return this.updateQueueHandler; + } + + /** + * Gets the string formatter to use for formatting messages + * + * @return

The string formatter

+ */ @NotNull public StringFormatter getStringFormatter() { return this.stringFormatter; } + /** + * Gets the default sign update delay + * + * @return

The sign update delay

+ */ + public int getSignUpdateDelay() { + return this.signUpdateDelay; + } + @Override public void onEnable() { instance = this; getConfig().options().copyDefaults(true); saveConfig(); + Translator translator = new Translator(); translator.registerMessageCategory(PlaceholderSignMessage.SUCCESS_CLICK_SIGN_TO_EDIT); translator.setColorConversion(ColorConversion.RGB); @@ -77,9 +113,15 @@ public final class PlaceholderSigns extends JavaPlugin { this.stringFormatter.setNamePrefix("#A5682A[&r&l"); this.stringFormatter.setNameSuffix("&r#A5682A]"); + signUpdateDelay = getConfig().getInt("defaultSignUpdateTicks", 100); + if (signUpdateDelay < 1) { + signUpdateDelay = 100; + } + this.signHandler = new PlaceholderSignHandler(); this.signHandler.load(); this.requestHandler = new PlaceholderSignRequestHandler(); + this.updateQueueHandler = new PlaceholderSignUpdateQueueHandler(); if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") == null) { getLogger().log(Level.WARNING, "Could not find PlaceholderAPI! This plugin is required."); @@ -87,18 +129,20 @@ public final class PlaceholderSigns extends JavaPlugin { return; } - // Update signs' placeholders every second - Bukkit.getScheduler().runTaskTimer(this, new SignUpdate(this.signHandler), 20 * 10, 20 * 5); + Bukkit.getScheduler().runTaskTimer(this, new SignUpdate(this.signHandler), 20, 1); - Bukkit.getPluginManager().registerEvents(new SignBreakListener(), this); - Bukkit.getPluginManager().registerEvents(new SignTextListener(), this); - Bukkit.getPluginManager().registerEvents(new SignClickListener(), this); + PluginManager pluginManager = Bukkit.getPluginManager(); + pluginManager.registerEvents(new SignBreakListener(), this); + pluginManager.registerEvents(new SignTextListener(), this); + pluginManager.registerEvents(new SignClickListener(), this); + pluginManager.registerEvents(new ChunkListener(), this); registerCommand("setSignLine", new EditSignCommand()); registerCommand("viewSign", new ViewSignCommand()); registerCommand("copySign", new CopySignCommand()); registerCommand("unWaxSign", new UnWaxSignCommand()); registerCommand("copySignText", new CopySignTextCommand()); + registerCommand("setPlaceholderUpdateDelay", new SetPlaceholderUpdateDelayCommand()); } @Override diff --git a/src/main/java/net/knarcraft/placeholdersigns/command/SetPlaceholderUpdateDelayCommand.java b/src/main/java/net/knarcraft/placeholdersigns/command/SetPlaceholderUpdateDelayCommand.java new file mode 100644 index 0000000..b273838 --- /dev/null +++ b/src/main/java/net/knarcraft/placeholdersigns/command/SetPlaceholderUpdateDelayCommand.java @@ -0,0 +1,77 @@ +package net.knarcraft.placeholdersigns.command; + +import net.knarcraft.knarlib.formatting.StringFormatter; +import net.knarcraft.knarlib.util.TabCompletionHelper; +import net.knarcraft.placeholdersigns.PlaceholderSigns; +import net.knarcraft.placeholdersigns.config.PlaceholderSignMessage; +import net.knarcraft.placeholdersigns.container.PlaceholderSign; +import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler; +import net.knarcraft.placeholdersigns.util.TabCompleteHelper; +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class SetPlaceholderUpdateDelayCommand implements TabExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, + @NotNull String[] arguments) { + PlaceholderSigns placeholderSigns = PlaceholderSigns.getInstance(); + StringFormatter stringFormatter = placeholderSigns.getStringFormatter(); + if (!(commandSender instanceof Player player)) { + stringFormatter.displayErrorMessage(commandSender, PlaceholderSignMessage.ERROR_PLAYER_ONLY); + return false; + } + + if (arguments.length < 1) { + return false; + } + + Block targetBlock = player.getTargetBlockExact(7); + if (targetBlock == null || !(targetBlock.getState() instanceof Sign sign)) { + stringFormatter.displayErrorMessage(commandSender, PlaceholderSignMessage.ERROR_NOT_LOOKING_AT_SIGN); + return false; + } + + Integer updateDelay = null; + try { + updateDelay = Integer.parseInt(arguments[0]); + } catch (NumberFormatException exception) { + if (!arguments[0].equalsIgnoreCase("null")) { + return false; + } + } + + PlaceholderSignHandler signHandler = placeholderSigns.getSignHandler(); + PlaceholderSign placeholderSign = signHandler.getFromLocation(sign.getLocation()); + if (placeholderSign != null) { + placeholderSign.setUpdateDelay(updateDelay); + signHandler.save(); + stringFormatter.displaySuccessMessage(player, PlaceholderSignMessage.SUCCESS_UPDATE_DELAY_CHANGED); + return true; + } else { + stringFormatter.displayErrorMessage(player, PlaceholderSignMessage.ERROR_NOT_LOOKING_AT_PLACEHOLDER_SIGN); + return false; + } + } + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, + @NotNull String[] arguments) { + if (arguments.length == 1) { + return TabCompletionHelper.filterMatchingStartsWith(TabCompleteHelper.getDelays(), arguments[0]); + } else { + return new ArrayList<>(); + } + } + +} diff --git a/src/main/java/net/knarcraft/placeholdersigns/command/ViewSignCommand.java b/src/main/java/net/knarcraft/placeholdersigns/command/ViewSignCommand.java index 38c9d2d..dc184a2 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/command/ViewSignCommand.java +++ b/src/main/java/net/knarcraft/placeholdersigns/command/ViewSignCommand.java @@ -160,7 +160,7 @@ public class ViewSignCommand implements TabExecutor { return getSignText(lines, raw); } - Map placeholders = placeholderSign.placeholders().get(side); + Map placeholders = placeholderSign.getPlaceholders().get(side); if (placeholders != null) { for (Map.Entry entry : placeholders.entrySet()) { lines[entry.getKey()] = entry.getValue(); diff --git a/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java b/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java index 4365b20..fd740da 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java +++ b/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java @@ -67,6 +67,16 @@ public enum PlaceholderSignMessage implements TranslatableMessage { * The message displayed when a sign has been successfully un-waxed */ SUCCESS_SIGN_UN_WAXED, + + /** + * The message displayed when the player isn't looking at a placeholder sign when required + */ + ERROR_NOT_LOOKING_AT_PLACEHOLDER_SIGN, + + /** + * The message displayed when a placeholder sign update delay has been changed + */ + SUCCESS_UPDATE_DELAY_CHANGED, ; @Override diff --git a/src/main/java/net/knarcraft/placeholdersigns/container/PlaceholderSign.java b/src/main/java/net/knarcraft/placeholdersigns/container/PlaceholderSign.java index c1306fb..dd5301d 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/container/PlaceholderSign.java +++ b/src/main/java/net/knarcraft/placeholdersigns/container/PlaceholderSign.java @@ -3,15 +3,78 @@ package net.knarcraft.placeholdersigns.container; import org.bukkit.Location; import org.bukkit.block.sign.Side; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Map; /** * A sign containing one or more placeholders - * - * @param location

The location of the sign

- * @param placeholders

The original placeholders typed on the sign

*/ -public record PlaceholderSign(@NotNull Location location, @NotNull Map> placeholders) { +public final class PlaceholderSign { + + private final @NotNull Location location; + private final @NotNull Map> placeholders; + private @Nullable Integer updateDelay; + + /** + * Instantiates a new placeholder sign + * + * @param location

The location of the sign

+ * @param placeholders

The original placeholders typed on the sign

+ * @param updateDelay

The delay in ticks between each time this sign's placeholders are updated

+ */ + public PlaceholderSign(@NotNull Location location, @NotNull Map> placeholders, + @Nullable Integer updateDelay) { + this.location = location; + this.placeholders = placeholders; + this.updateDelay = updateDelay; + } + + /** + * Gets the location of this placeholder sign + * + * @return

The location of this placeholder sign

+ */ + @NotNull + public Location getLocation() { + return location; + } + + /** + * Gets the placeholders stored on this sign + * + * @return

The stored placeholders

+ */ + @NotNull + public Map> getPlaceholders() { + return placeholders; + } + + /** + * Gets the update delay for this placeholder sign + * + * @return

The update delay

+ */ + @Nullable + public Integer getUpdateDelay() { + return updateDelay; + } + + /** + * Sets the update delay for this placeholder sign + * + * @param updateDelay

The new update delay

+ */ + public void setUpdateDelay(@Nullable Integer updateDelay) { + this.updateDelay = updateDelay; + } + + @Override + public boolean equals(@NotNull Object object) { + if (object instanceof PlaceholderSign otherSign) { + return this.location.equals(otherSign.location); + } + return false; + } } diff --git a/src/main/java/net/knarcraft/placeholdersigns/container/QueuedPlaceholderSign.java b/src/main/java/net/knarcraft/placeholdersigns/container/QueuedPlaceholderSign.java new file mode 100644 index 0000000..664b23e --- /dev/null +++ b/src/main/java/net/knarcraft/placeholdersigns/container/QueuedPlaceholderSign.java @@ -0,0 +1,19 @@ +package net.knarcraft.placeholdersigns.container; + +import org.jetbrains.annotations.NotNull; + +/** + * A placeholder sign that's queued to be updated + * + * @param placeholderSign

The queued placeholder sign

+ * @param updateTimestamp

The timestamp (long milliseconds) that the placeholder sign should be updated at

+ */ +public record QueuedPlaceholderSign(@NotNull PlaceholderSign placeholderSign, + long updateTimestamp) implements Comparable { + + @Override + public int compareTo(@NotNull QueuedPlaceholderSign other) { + return Long.compare(this.updateTimestamp, other.updateTimestamp); + } + +} diff --git a/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignHandler.java b/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignHandler.java index fcaeb98..7f1a6ee 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignHandler.java +++ b/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignHandler.java @@ -3,6 +3,7 @@ package net.knarcraft.placeholdersigns.handler; import net.knarcraft.placeholdersigns.PlaceholderSigns; import net.knarcraft.placeholdersigns.container.PlaceholderSign; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.sign.Side; @@ -29,15 +30,7 @@ public class PlaceholderSignHandler { private Set placeholderSigns; private Map locationLookup; - - /** - * Gets all registered signs - * - * @return

All registered signs

- */ - public @NotNull Set getSigns() { - return new HashSet<>(placeholderSigns); - } + private Map> signsInChunk; /** * Gets a placeholder sign from the given location @@ -57,8 +50,11 @@ public class PlaceholderSignHandler { */ public void registerSign(@NotNull PlaceholderSign sign) { this.placeholderSigns.add(sign); - locationLookup.put(sign.location(), sign); - save(); + this.locationLookup.put(sign.getLocation(), sign); + + Chunk chunk = sign.getLocation().getChunk(); + this.signsInChunk.putIfAbsent(chunk, new HashSet<>()); + this.signsInChunk.get(chunk).add(sign); } /** @@ -67,9 +63,24 @@ public class PlaceholderSignHandler { * @param sign

The sign to un-register

*/ public void unregisterSign(@NotNull PlaceholderSign sign) { - locationLookup.remove(sign.location()); + this.locationLookup.remove(sign.getLocation()); this.placeholderSigns.remove(sign); - save(); + this.signsInChunk.get(sign.getLocation().getChunk()).remove(sign); + } + + /** + * Gets all placeholder signs in the given chunk + * + * @param chunk

The chunk to check

+ * @return

All placeholder signs in the chunk

+ */ + @NotNull + public Set getFromChunk(@NotNull Chunk chunk) { + if (this.signsInChunk.containsKey(chunk)) { + return this.signsInChunk.get(chunk); + } else { + return new HashSet<>(); + } } /** @@ -78,6 +89,7 @@ public class PlaceholderSignHandler { public void load() { this.placeholderSigns = new HashSet<>(); this.locationLookup = new HashMap<>(); + this.signsInChunk = new HashMap<>(); YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile); ConfigurationSection signSection = configuration.getConfigurationSection("signs"); @@ -104,9 +116,16 @@ public class PlaceholderSignHandler { allPlaceholders.put(Side.FRONT, frontPlaceholders); allPlaceholders.put(Side.BACK, backPlaceholders); - PlaceholderSign sign = new PlaceholderSign(signLocation, allPlaceholders); - this.placeholderSigns.add(sign); - this.locationLookup.put(signLocation, sign); + String updateDelayKey = key + ".updateDelay"; + int updateDelay = -1; + if (signSection.contains(updateDelayKey)) { + updateDelay = signSection.getInt(updateDelayKey, -1); + } + if (updateDelay < 1) { + updateDelay = PlaceholderSigns.getInstance().getSignUpdateDelay(); + } + + registerSign(new PlaceholderSign(signLocation, allPlaceholders, updateDelay)); } } @@ -153,7 +172,7 @@ public class PlaceholderSignHandler { * @param sign

The sign to save

*/ private void saveSign(@NotNull ConfigurationSection section, @NotNull PlaceholderSign sign) { - Location location = sign.location(); + Location location = sign.getLocation(); if (location.getWorld() == null) { return; } @@ -163,19 +182,21 @@ public class PlaceholderSignHandler { String frontKey = key + ".placeholders.front"; String backKey = key + ".placeholders.back"; - Map frontPlaceholders = sign.placeholders().get(Side.FRONT); + Map frontPlaceholders = sign.getPlaceholders().get(Side.FRONT); if (frontPlaceholders != null) { for (Map.Entry entry : frontPlaceholders.entrySet()) { section.set(frontKey + "." + entry.getKey(), entry.getValue()); } } - Map backPlaceholders = sign.placeholders().get(Side.BACK); + Map backPlaceholders = sign.getPlaceholders().get(Side.BACK); if (backPlaceholders != null) { for (Map.Entry entry : backPlaceholders.entrySet()) { section.set(backKey + "." + entry.getKey(), entry.getValue()); } } + + section.set(key + ".updateDelay", sign.getUpdateDelay()); } } diff --git a/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignUpdateQueueHandler.java b/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignUpdateQueueHandler.java new file mode 100644 index 0000000..a1b6878 --- /dev/null +++ b/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignUpdateQueueHandler.java @@ -0,0 +1,77 @@ +package net.knarcraft.placeholdersigns.handler; + +import net.knarcraft.placeholdersigns.container.PlaceholderSign; +import net.knarcraft.placeholdersigns.container.QueuedPlaceholderSign; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.PriorityQueue; + +/** + * A handler for dealing with the placeholder sign update queue + */ +public class PlaceholderSignUpdateQueueHandler { + + private final PriorityQueue signUpdateQueue; + + /** + * Instantiates a new placeholder sign update queue handler + */ + public PlaceholderSignUpdateQueueHandler() { + this.signUpdateQueue = new PriorityQueue<>(); + } + + /** + * Polls the queue for any placeholder signs due for update + * + *

No placeholder sign will be returned unless they are due to be updated.

+ * + * @return

The next queued placeholder sign, or null if no such placeholder sign exists

+ */ + @Nullable + public PlaceholderSign pollQueue() { + QueuedPlaceholderSign nextSign = signUpdateQueue.peek(); + if (nextSign == null) { + return null; + } + + long currentTime = System.currentTimeMillis(); + if (nextSign.updateTimestamp() < currentTime) { + nextSign = signUpdateQueue.poll(); + if (nextSign != null) { + return nextSign.placeholderSign(); + } + } + return null; + } + + /** + * Peeks at the next item in the queue + * + * @return

The next item in the queue, or null if empty

+ */ + @Nullable + public QueuedPlaceholderSign peekQueue() { + return signUpdateQueue.peek(); + } + + /** + * Adds the specified queued sign to the queue + * + * @param queuedSign

The queued sign to queue

+ */ + public void queueSign(@NotNull QueuedPlaceholderSign queuedSign) { + this.signUpdateQueue.add(queuedSign); + } + + /** + * ' + * Removes the specified placeholder sign from the queue + * + * @param placeholderSign

The placeholder sign to remove

+ */ + public void unQueueSign(@NotNull PlaceholderSign placeholderSign) { + this.signUpdateQueue.removeIf((item) -> item.placeholderSign().equals(placeholderSign)); + } + +} diff --git a/src/main/java/net/knarcraft/placeholdersigns/listener/ChunkListener.java b/src/main/java/net/knarcraft/placeholdersigns/listener/ChunkListener.java new file mode 100644 index 0000000..80849bb --- /dev/null +++ b/src/main/java/net/knarcraft/placeholdersigns/listener/ChunkListener.java @@ -0,0 +1,38 @@ +package net.knarcraft.placeholdersigns.listener; + +import net.knarcraft.placeholdersigns.PlaceholderSigns; +import net.knarcraft.placeholdersigns.container.PlaceholderSign; +import net.knarcraft.placeholdersigns.container.QueuedPlaceholderSign; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * A listener for the loading and unloading of chunks with placeholder signs + */ +public class ChunkListener implements Listener { + + @EventHandler + public void onChunkLoad(@NotNull ChunkLoadEvent event) { + PlaceholderSigns instance = PlaceholderSigns.getInstance(); + Set signsAtChunk = instance.getSignHandler().getFromChunk(event.getChunk()); + long queueTime = System.currentTimeMillis(); + for (PlaceholderSign sign : signsAtChunk) { + instance.getUpdateQueueHandler().queueSign(new QueuedPlaceholderSign(sign, queueTime++)); + } + } + + @EventHandler + public void onChunkUnload(@NotNull ChunkUnloadEvent event) { + PlaceholderSigns instance = PlaceholderSigns.getInstance(); + Set signsAtChunk = instance.getSignHandler().getFromChunk(event.getChunk()); + for (PlaceholderSign sign : signsAtChunk) { + instance.getUpdateQueueHandler().unQueueSign(sign); + } + } + +} diff --git a/src/main/java/net/knarcraft/placeholdersigns/listener/SignBreakListener.java b/src/main/java/net/knarcraft/placeholdersigns/listener/SignBreakListener.java index f750dce..07c9c31 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/listener/SignBreakListener.java +++ b/src/main/java/net/knarcraft/placeholdersigns/listener/SignBreakListener.java @@ -27,6 +27,7 @@ public class SignBreakListener implements Listener { PlaceholderSign sign = signHandler.getFromLocation(block.getLocation()); if (sign != null) { signHandler.unregisterSign(sign); + signHandler.save(); } } diff --git a/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java b/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java index 31c9a86..1e58468 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java +++ b/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java @@ -133,7 +133,7 @@ public class SignClickListener implements Listener { String text = signTextCopyRequest.sign().getSide(signTextCopyRequest.side()).getLine(sourceLine); if (sourcePlaceholderSign != null) { - Map> placeholders = sourcePlaceholderSign.placeholders(); + Map> placeholders = sourcePlaceholderSign.getPlaceholders(); if (placeholders.containsKey(sourceSide) && placeholders.get(sourceSide).containsKey(sourceLine)) { text = placeholders.get(sourceSide).get(sourceLine); } @@ -162,12 +162,12 @@ public class SignClickListener implements Listener { // Remove old placeholders from the sign side Map oldPlaceholders = null; if (targetPlaceholderSign != null) { - oldPlaceholders = targetPlaceholderSign.placeholders().remove(clickedSide); + oldPlaceholders = targetPlaceholderSign.getPlaceholders().remove(clickedSide); } if (pasteSignSide(sourceSide, clickedSide, sourceSign, targetSign, sourcePlaceholderSign, player) && targetPlaceholderSign != null) { // Restore the old placeholders if the sign change didn't finish - targetPlaceholderSign.placeholders().put(clickedSide, oldPlaceholders); + targetPlaceholderSign.getPlaceholders().put(clickedSide, oldPlaceholders); } // Save any placeholder changes @@ -307,7 +307,7 @@ public class SignClickListener implements Listener { @NotNull Player player) { String[] sideLines = signSide.getLines(); if (placeholderSign != null) { - Map> placeholders = placeholderSign.placeholders(); + Map> placeholders = placeholderSign.getPlaceholders(); loadPlaceholders(sourceSide, sideLines, placeholders); } @@ -385,7 +385,7 @@ public class SignClickListener implements Listener { String oldPlaceholder = null; if (targetPlaceholderSign != null) { // Remove the old placeholder - oldPlaceholder = targetPlaceholderSign.placeholders().get(targetSide).remove(destinationLine); + oldPlaceholder = targetPlaceholderSign.getPlaceholders().get(targetSide).remove(destinationLine); } lines[destinationLine] = newText; @@ -396,7 +396,7 @@ public class SignClickListener implements Listener { if (changeEvent.isCancelled()) { if (targetPlaceholderSign != null) { // Restore the old placeholder if the action didn't complete - targetPlaceholderSign.placeholders().get(targetSide).put(destinationLine, oldPlaceholder); + targetPlaceholderSign.getPlaceholders().get(targetSide).put(destinationLine, oldPlaceholder); } PlaceholderSigns.getInstance().getStringFormatter().displayErrorMessage(player, PlaceholderSignMessage.ERROR_CANCELLED_BY_PROTECTION); diff --git a/src/main/java/net/knarcraft/placeholdersigns/listener/SignTextListener.java b/src/main/java/net/knarcraft/placeholdersigns/listener/SignTextListener.java index 502be89..11cc9a8 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/listener/SignTextListener.java +++ b/src/main/java/net/knarcraft/placeholdersigns/listener/SignTextListener.java @@ -61,11 +61,12 @@ public class SignTextListener implements Listener { placeholderSide.put(event.getSide(), placeholders); // Register a new placeholder sign - PlaceholderSign placeholderSign = new PlaceholderSign(event.getBlock().getLocation(), placeholderSide); + PlaceholderSign placeholderSign = new PlaceholderSign(event.getBlock().getLocation(), placeholderSide, null); signHandler.registerSign(placeholderSign); + signHandler.save(); } else if (!placeholders.isEmpty()) { // Overwrite the placeholders of the existing placeholder sign - Map existing = existingSign.placeholders().computeIfAbsent(event.getSide(), k -> new HashMap<>()); + Map existing = existingSign.getPlaceholders().computeIfAbsent(event.getSide(), k -> new HashMap<>()); existing.putAll(placeholders); signHandler.save(); } diff --git a/src/main/java/net/knarcraft/placeholdersigns/runnable/SignUpdate.java b/src/main/java/net/knarcraft/placeholdersigns/runnable/SignUpdate.java index 93cd251..f26ed29 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/runnable/SignUpdate.java +++ b/src/main/java/net/knarcraft/placeholdersigns/runnable/SignUpdate.java @@ -3,8 +3,11 @@ package net.knarcraft.placeholdersigns.runnable; import me.clip.placeholderapi.PlaceholderAPI; import net.knarcraft.knarlib.property.ColorConversion; import net.knarcraft.knarlib.util.ColorHelper; +import net.knarcraft.placeholdersigns.PlaceholderSigns; import net.knarcraft.placeholdersigns.container.PlaceholderSign; +import net.knarcraft.placeholdersigns.container.QueuedPlaceholderSign; import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler; +import net.knarcraft.placeholdersigns.handler.PlaceholderSignUpdateQueueHandler; import org.bukkit.Location; import org.bukkit.block.Sign; import org.bukkit.block.sign.Side; @@ -31,40 +34,67 @@ public class SignUpdate implements Runnable { @Override public void run() { - for (PlaceholderSign placeholderSign : signHandler.getSigns()) { - // Ignore signs away from players - Location location = placeholderSign.location(); - if (!location.getChunk().isLoaded()) { - continue; + PlaceholderSigns instance = PlaceholderSigns.getInstance(); + PlaceholderSignUpdateQueueHandler queueHandler = instance.getUpdateQueueHandler(); + + long startTime = System.nanoTime(); + long currentTime = System.currentTimeMillis(); + while (queueHandler.peekQueue() != null && System.nanoTime() - startTime < 25000000) { + PlaceholderSign placeholderSign = instance.getUpdateQueueHandler().pollQueue(); + if (placeholderSign == null) { + break; } - // If no longer a sign, remove - if (!(location.getBlock().getState() instanceof Sign sign)) { - signHandler.unregisterSign(placeholderSign); - continue; - } - - // Update placeholders - SignSide front = sign.getSide(Side.FRONT); - SignSide back = sign.getSide(Side.BACK); - Map> placeholders = placeholderSign.placeholders(); - String[] frontLines = front.getLines(); - String[] backLines = back.getLines(); - - // Only update the sign if the text has changed - boolean updateNecessary = false; - if (placeholders.get(Side.FRONT) != null) { - updateNecessary |= updatePlaceholders(frontLines, placeholders.get(Side.FRONT), front); - } - if (placeholders.get(Side.BACK) != null) { - updateNecessary |= updatePlaceholders(backLines, placeholders.get(Side.BACK), back); - } - if (updateNecessary) { - sign.update(); - } + updatePlaceholderSign(placeholderSign, currentTime); } } + /** + * Updates the contents of a single placeholder sign + * + * @param placeholderSign

The placeholder sign to update

+ * @param currentTime

The current time, used for re-queuing the sign

+ */ + private void updatePlaceholderSign(@NotNull PlaceholderSign placeholderSign, long currentTime) { + PlaceholderSigns instance = PlaceholderSigns.getInstance(); + + // Ignore signs away from players + Location location = placeholderSign.getLocation(); + + // If no longer a sign, remove + if (!(location.getBlock().getState() instanceof Sign sign)) { + this.signHandler.unregisterSign(placeholderSign); + this.signHandler.save(); + return; + } + + // Update placeholders + SignSide front = sign.getSide(Side.FRONT); + SignSide back = sign.getSide(Side.BACK); + Map> placeholders = placeholderSign.getPlaceholders(); + String[] frontLines = front.getLines(); + String[] backLines = back.getLines(); + + // Only update the sign if the text has changed + boolean updateNecessary = false; + if (placeholders.get(Side.FRONT) != null) { + updateNecessary |= updatePlaceholders(frontLines, placeholders.get(Side.FRONT), front); + } + if (placeholders.get(Side.BACK) != null) { + updateNecessary |= updatePlaceholders(backLines, placeholders.get(Side.BACK), back); + } + if (updateNecessary) { + sign.update(); + } + + Integer updateDelay = placeholderSign.getUpdateDelay(); + if (updateDelay == null) { + updateDelay = instance.getSignUpdateDelay(); + } + + instance.getUpdateQueueHandler().queueSign(new QueuedPlaceholderSign(placeholderSign, currentTime + (updateDelay * 50))); + } + /** * Updates the values of placeholders on a sign * diff --git a/src/main/java/net/knarcraft/placeholdersigns/util/TabCompleteHelper.java b/src/main/java/net/knarcraft/placeholdersigns/util/TabCompleteHelper.java index 9dd9a64..ff751fa 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/util/TabCompleteHelper.java +++ b/src/main/java/net/knarcraft/placeholdersigns/util/TabCompleteHelper.java @@ -13,6 +13,7 @@ public final class TabCompleteHelper { private static final List lineNumbers; private static final List signSides; + private static final List updateDelays; static { lineNumbers = new ArrayList<>(); @@ -24,12 +25,24 @@ public final class TabCompleteHelper { for (Side side : Side.values()) { signSides.add(side.name()); } + + updateDelays = List.of("null", "1", "5", "10", "20", "40", "60", "80", "100"); } private TabCompleteHelper() { } + /** + * Gets possible sign update delays + * + * @return

Possible sign update delays

+ */ + @NotNull + public static List getDelays() { + return new ArrayList<>(updateDelays); + } + /** * Gets possible sign line numbers * diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7858036..08d96ea 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,2 +1,8 @@ # The chosen language for PlaceholderSigns. You can use "en" or any custom language specified in strings.yml -language: en \ No newline at end of file +language: en + +# How often to update placeholders by default in ticks. 1 second = 20 ticks. The default is 100 ticks = 5 seconds. Note +# that only loaded chunks will have their signs updated to reduce lag and RAM usage from chunk loading. Still, it is +# recommended to set this to a higher value, and only reduce the update delay for individual signs where updated +# information is critical. 1 is the minimal accepted value. +defaultSignUpdateTicks: 100 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 49e65fc..c76789d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -21,6 +21,7 @@ commands: unWaxSign: usage: / permission: placeholdersigns.unwax + description: Removes the wax from a waxed sign copySignText: usage: | / [side] [sourceLine] [destinationLine] @@ -29,6 +30,10 @@ commands: destinationLine = The destination sign line to overwrite. If not set, it's the same as sourceLine permission: placeholdersigns.copy.use description: Copies all text, or a single line, from one sign to another, either the specified side or both + setPlaceholderUpdateDelay: + usage: / + permission: placeholdersigns.setdelay + description: Sets the update delay for a placeholder sign permissions: placeholdersigns.*: @@ -39,6 +44,7 @@ permissions: - placeholdersigns.view - placeholdersigns.copy - placeholdersigns.unwax + - placeholdersigns.setdelay default: op placeholdersigns.minimal: description: Allows minimal access to /setSignLine, /copySign, /copySignText and /viewSign @@ -78,4 +84,7 @@ permissions: default: false placeholdersigns.unwax: description: Allows a player to use the /unWax command + default: false + placeholdersigns.setdelay: + description: Allows a player to use the /setPlaceholderUpdateDelay command default: false \ No newline at end of file diff --git a/src/main/resources/strings.yml b/src/main/resources/strings.yml index 61c8c9f..6f3654d 100644 --- a/src/main/resources/strings.yml +++ b/src/main/resources/strings.yml @@ -18,4 +18,6 @@ en: SUCCESS_CLICK_SIGN_TO_PASTE: "&7Click the sign you want to paste onto" SUCCESS_SIGN_PASTED: "&7Sign pasted!" ERROR_CANCELLED_BY_PROTECTION: "A protection plugin blocked the sign change" - SUCCESS_SIGN_UN_WAXED: "&7The sign was successfully un-waxed" \ No newline at end of file + SUCCESS_SIGN_UN_WAXED: "&7The sign was successfully un-waxed" + ERROR_NOT_LOOKING_AT_PLACEHOLDER_SIGN: "You are not currently looking at a placeholder sign" + SUCCESS_UPDATE_DELAY_CHANGED: "&7The placeholder sign's update delay was successfully updated" \ No newline at end of file