diff --git a/README.md b/README.md index 3293e49..79a91b7 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,21 @@ won't be changed unless the player passes all world protection checks. ## Commands -| Command | Arguments | Description | -|-----------|-----------------------------|-----------------------------------------------------------------------------------------------| -| /editSign | \ \ \ ... | Sets the text of the sign line (1-4) to the given input. Then right-click the sign to update. | +| 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] | Allows the player to view the full contents and details of the looked at sign. If "raw" is true, formatting codes are displayed. If placeholders is true, stored placeholders are displayed. | +| /copySign | | Allows the player to copy the sign they are currently looking at to another sign, including placeholders, formatting codes, dye and waxed state. | ## Permissions | Permission | Description | |------------------------------------|---------------------------------------------------------------------------------------------------------------| | placeholdersigns.* | Gives all permissions. | -| placeholdersigns.edit | Allows unrestricted use of the /editSign command. | -| placeholdersigns.edit.use | Allows use of the /editSign command. | -| placeholdersigns.edit.bypass-waxed | Allows use of the /editSign command on a waxed sign. | -| placeholdersigns.placeholder | Allows a player to make signs containing placeholders. Without this, placeholders are treated as normal text. | \ No newline at end of file +| placeholdersigns.edit | Allows unrestricted use of the /setSignLine command. | +| placeholdersigns.edit.use | Allows use of the /setSignLine command. | +| placeholdersigns.edit.bypass-waxed | Allows use of the /setSignLine command on a waxed sign. | +| placeholdersigns.placeholder | Allows a player to make signs containing placeholders. Without this, placeholders are treated as normal text. | +| placeholdersigns.copy | Allows unrestricted use of the /copySign command. | +| placeholdersigns.copy.use | Allows use of the /copySign command. | +| placeholdersigns.copy.bypass-waxed | Allows pasting a sign copied with /copySign onto a waxed sign. | \ 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 953f885..3300f62 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/PlaceholderSigns.java +++ b/src/main/java/net/knarcraft/placeholdersigns/PlaceholderSigns.java @@ -3,8 +3,8 @@ package net.knarcraft.placeholdersigns; import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.knarlib.formatting.Translator; import net.knarcraft.knarlib.property.ColorConversion; +import net.knarcraft.placeholdersigns.command.CopySignCommand; import net.knarcraft.placeholdersigns.command.EditSignCommand; -import net.knarcraft.placeholdersigns.command.ViewPlaceholderSignCommand; import net.knarcraft.placeholdersigns.command.ViewSignCommand; import net.knarcraft.placeholdersigns.config.PlaceholderSignMessage; import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler; @@ -92,11 +92,9 @@ public final class PlaceholderSigns extends JavaPlugin { Bukkit.getPluginManager().registerEvents(new SignTextListener(), this); Bukkit.getPluginManager().registerEvents(new SignClickListener(), this); - registerCommand("editSign", new EditSignCommand()); - registerCommand("viewSign", new ViewSignCommand(false)); - registerCommand("viewSignRaw", new ViewSignCommand(true)); - registerCommand("viewPlaceholderSign", new ViewPlaceholderSignCommand(false)); - registerCommand("viewPlaceholderSignRaw", new ViewPlaceholderSignCommand(true)); + registerCommand("setSignLine", new EditSignCommand()); + registerCommand("viewSign", new ViewSignCommand()); + registerCommand("copySign", new CopySignCommand()); } @Override diff --git a/src/main/java/net/knarcraft/placeholdersigns/command/CopySignCommand.java b/src/main/java/net/knarcraft/placeholdersigns/command/CopySignCommand.java new file mode 100644 index 0000000..be5ebd5 --- /dev/null +++ b/src/main/java/net/knarcraft/placeholdersigns/command/CopySignCommand.java @@ -0,0 +1,54 @@ +package net.knarcraft.placeholdersigns.command; + +import net.knarcraft.knarlib.formatting.StringFormatter; +import net.knarcraft.placeholdersigns.PlaceholderSigns; +import net.knarcraft.placeholdersigns.config.PlaceholderSignMessage; +import net.knarcraft.placeholdersigns.container.SignCopyRequest; +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; + +/** + * A command for copying signs, including placeholders + */ +public class CopySignCommand implements TabExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, + @NotNull String[] strings) { + PlaceholderSigns placeholderSigns = PlaceholderSigns.getInstance(); + StringFormatter stringFormatter = placeholderSigns.getStringFormatter(); + if (!(commandSender instanceof Player player)) { + stringFormatter.displayErrorMessage(commandSender, PlaceholderSignMessage.ERROR_PLAYER_ONLY); + 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; + } + + SignCopyRequest signCopyRequest = new SignCopyRequest(player, sign); + placeholderSigns.getRequestHandler().addSignCopyRequest(signCopyRequest); + + stringFormatter.displaySuccessMessage(commandSender, PlaceholderSignMessage.SUCCESS_CLICK_SIGN_TO_PASTE); + return true; + } + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, + @NotNull String[] strings) { + return new ArrayList<>(); + } + +} diff --git a/src/main/java/net/knarcraft/placeholdersigns/command/EditSignCommand.java b/src/main/java/net/knarcraft/placeholdersigns/command/EditSignCommand.java index 06418e8..7631c8c 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/command/EditSignCommand.java +++ b/src/main/java/net/knarcraft/placeholdersigns/command/EditSignCommand.java @@ -1,5 +1,6 @@ package net.knarcraft.placeholdersigns.command; +import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.placeholdersigns.PlaceholderSigns; import net.knarcraft.placeholdersigns.config.PlaceholderSignMessage; import net.knarcraft.placeholdersigns.container.SignLineChangeRequest; @@ -30,7 +31,13 @@ public class EditSignCommand implements TabExecutor { @Override public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { - if (args.length < 1 || !(commandSender instanceof Player player)) { + PlaceholderSigns placeholderSigns = PlaceholderSigns.getInstance(); + StringFormatter stringFormatter = placeholderSigns.getStringFormatter(); + if (!(commandSender instanceof Player player)) { + stringFormatter.displayErrorMessage(commandSender, PlaceholderSignMessage.ERROR_PLAYER_ONLY); + return false; + } + if (args.length < 1) { return false; } @@ -61,11 +68,9 @@ public class EditSignCommand implements TabExecutor { // Register the line change request SignLineChangeRequest request = new SignLineChangeRequest(player, lineNumber - 1, builder.toString()); - PlaceholderSigns placeholderSigns = PlaceholderSigns.getInstance(); placeholderSigns.getRequestHandler().addSignChangeRequest(request); - placeholderSigns.getStringFormatter().displaySuccessMessage(commandSender, - PlaceholderSignMessage.SUCCESS_CLICK_SIGN_TO_EDIT); + stringFormatter.displaySuccessMessage(commandSender, PlaceholderSignMessage.SUCCESS_CLICK_SIGN_TO_EDIT); return true; } diff --git a/src/main/java/net/knarcraft/placeholdersigns/command/ViewPlaceholderSignCommand.java b/src/main/java/net/knarcraft/placeholdersigns/command/ViewPlaceholderSignCommand.java deleted file mode 100644 index eb403cb..0000000 --- a/src/main/java/net/knarcraft/placeholdersigns/command/ViewPlaceholderSignCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.knarcraft.placeholdersigns.command; - -import net.knarcraft.placeholdersigns.PlaceholderSigns; -import net.knarcraft.placeholdersigns.config.PlaceholderSignMessage; -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; - -/** - * A command for viewing placeholders on a sign - */ -public class ViewPlaceholderSignCommand implements TabExecutor { - - private final boolean raw; - - /** - * Instantiates a new view sign command - * - * @param raw

Whether this sign displays the raw text or not

- */ - public ViewPlaceholderSignCommand(boolean raw) { - this.raw = raw; - } - - @Override - public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, - @NotNull String[] args) { - if (!(commandSender instanceof Player player)) { - return false; - } - - // Register the sign view request - PlaceholderSigns.getInstance().getRequestHandler().addPlaceholderSignViewRequest(player, this.raw); - - PlaceholderSigns.getInstance().getStringFormatter().displaySuccessMessage(commandSender, - PlaceholderSignMessage.SUCCESS_CLICK_SIGN_TO_VIEW); - return true; - } - - @Nullable - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, - @NotNull String[] args) { - 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 d7b0deb..aee0663 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/command/ViewSignCommand.java +++ b/src/main/java/net/knarcraft/placeholdersigns/command/ViewSignCommand.java @@ -1,7 +1,19 @@ package net.knarcraft.placeholdersigns.command; +import net.knarcraft.knarlib.formatting.StringFormatter; +import net.knarcraft.knarlib.formatting.StringReplacer; +import net.knarcraft.knarlib.util.ColorHelper; +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.md_5.bungee.api.ChatColor; +import org.bukkit.DyeColor; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; @@ -11,43 +23,172 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** - * A command for viewing lines on a sign + * A generic sign view command */ public class ViewSignCommand implements TabExecutor { - private final boolean raw; - - /** - * Instantiates a new view sign command - * - * @param raw

Whether this sign displays the raw text or not

- */ - public ViewSignCommand(boolean raw) { - this.raw = raw; - } - @Override public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, - @NotNull String[] args) { + @NotNull String[] arguments) { + StringFormatter stringFormatter = PlaceholderSigns.getInstance().getStringFormatter(); if (!(commandSender instanceof Player player)) { + stringFormatter.displayErrorMessage(commandSender, PlaceholderSignMessage.ERROR_PLAYER_ONLY); return false; } - // Register the sign view request - PlaceholderSigns.getInstance().getRequestHandler().addSignViewRequest(player, this.raw); + boolean showRawText = true; + boolean showPlaceholders = true; + if (arguments.length > 0) { + showRawText = Boolean.parseBoolean(arguments[0]); + } + if (arguments.length > 1) { + showPlaceholders = Boolean.parseBoolean(arguments[1]); + } - PlaceholderSigns.getInstance().getStringFormatter().displaySuccessMessage(commandSender, - PlaceholderSignMessage.SUCCESS_CLICK_SIGN_TO_VIEW); + Block targetBlock = player.getTargetBlockExact(7); + if (targetBlock == null || !(targetBlock.getState() instanceof Sign sign)) { + stringFormatter.displayErrorMessage(commandSender, PlaceholderSignMessage.ERROR_NOT_LOOKING_AT_SIGN); + return false; + } + + printSign(sign, player, showRawText, showPlaceholders); return true; } @Nullable @Override public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, - @NotNull String[] args) { - return new ArrayList<>(); + @NotNull String[] arguments) { + if (arguments.length == 1) { + return getBooleanTabCompletions(arguments[0]); + } else if (arguments.length == 2) { + return getBooleanTabCompletions(arguments[1]); + } else { + return new ArrayList<>(); + } + } + + /** + * Gets boolean tab-completions + * + * @param filter

The input argument to filter by

+ * @return

The the resulting boolean tab-completions

+ */ + @NotNull + private List getBooleanTabCompletions(@NotNull String filter) { + return TabCompletionHelper.filterMatchingStartsWith(List.of("true", "false"), filter); + } + + /** + * Prints the current contents of a sign to a player + * + * @param sign

The sign to print

+ * @param player

The player to display the contents to

+ */ + private void printSign(@NotNull Sign sign, @NotNull Player player, boolean showRawText, boolean showPlaceholders) { + Location location = sign.getLocation(); + SignSide front = sign.getSide(Side.FRONT); + SignSide back = sign.getSide(Side.BACK); + String frontLines = showPlaceholders ? getPlaceholderSignText(location, front, Side.FRONT, showRawText) : + getSignText(front.getLines(), showRawText); + String backLines = showPlaceholders ? getPlaceholderSignText(location, back, Side.BACK, showRawText) : + getSignText(back.getLines(), showRawText); + + StringFormatter stringFormatter = PlaceholderSigns.getInstance().getStringFormatter(); + StringReplacer replacer = new StringReplacer(stringFormatter.getUnformattedColoredMessage( + PlaceholderSignMessage.SUCCESS_SIGN_CONTENTS)); + replacer.add("{frontLines}", frontLines); + replacer.add("{backLines}", backLines); + replacer.add("{frontDye}", getDye(front)); + replacer.add("{frontGlow}", getStatus(front.isGlowingText())); + replacer.add("{backDye}", getDye(back)); + replacer.add("{backGlow}", getStatus(back.isGlowingText())); + replacer.add("{waxed}", getStatus(sign.isWaxed())); + player.sendMessage(replacer.replace()); + } + + /** + * Gets a description of the status of a sign property + * + * @param isTrue

Whether the property is true

+ * @return

A description of the property's current status

+ */ + @NotNull + private String getStatus(boolean isTrue) { + StringFormatter stringFormatter = PlaceholderSigns.getInstance().getStringFormatter(); + if (isTrue) { + return stringFormatter.getUnformattedColoredMessage(PlaceholderSignMessage.SIGN_PROPERTY_CONFIRM); + } else { + return stringFormatter.getUnformattedColoredMessage(PlaceholderSignMessage.SIGN_PROPERTY_DENY); + } + } + + /** + * Gets a description of a dye applied to a sign + * + * @param signSide

The sign side to get the dye of

+ * @return

The description of the applied dye

+ */ + @NotNull + private String getDye(@NotNull SignSide signSide) { + DyeColor dyeColor = signSide.getColor(); + if (dyeColor == null) { + return "None"; + } + + ChatColor color = ColorHelper.fromColor(dyeColor.getColor()); + return color + signSide.getColor().name(); + } + + /** + * Gets text from a sign side, and appends it to the given string builder + * + * @param signLocation

The location of the sign

+ * @param signSide

The sign side to get text from

+ * @param side

The side of the sign to get placeholders from

+ * @param raw

Whether to get the raw text with used formatting codes

+ */ + @NotNull + private String getPlaceholderSignText(@NotNull Location signLocation, @NotNull SignSide signSide, + @NotNull Side side, boolean raw) { + String[] lines = signSide.getLines(); + PlaceholderSign placeholderSign = PlaceholderSigns.getInstance().getSignHandler().getFromLocation(signLocation); + if (placeholderSign == null) { + return getSignText(lines, raw); + } + + Map placeholders = placeholderSign.placeholders().get(side); + for (Map.Entry entry : placeholders.entrySet()) { + lines[entry.getKey()] = entry.getValue(); + } + + return getSignText(lines, raw); + } + + /** + * Gets text from a sign side, and appends it to the given string builder + * + * @param lines

The lines on the sign

+ * @param raw

Whether to get the raw text with used formatting codes

+ */ + @NotNull + private String getSignText(@NotNull String[] lines, boolean raw) { + StringBuilder output = new StringBuilder(); + for (int i = 0; i < 4; i++) { + output.append(i + 1).append(". "); + + String line = lines[i]; + if (raw) { + output.append(line.replace(ChatColor.COLOR_CHAR, '&')); + } else { + output.append(line); + } + output.append(ChatColor.COLOR_CHAR).append("r\n"); + } + return output.toString(); } } diff --git a/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java b/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java index bcb9085..870db98 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java +++ b/src/main/java/net/knarcraft/placeholdersigns/config/PlaceholderSignMessage.java @@ -13,11 +13,6 @@ public enum PlaceholderSignMessage implements TranslatableMessage { */ SUCCESS_CLICK_SIGN_TO_EDIT, - /** - * The message to display when waiting for a sign selection (view) - */ - SUCCESS_CLICK_SIGN_TO_VIEW, - /** * The message displayed when a player tries to edit a waxed sign without the necessary permission */ @@ -42,6 +37,31 @@ public enum PlaceholderSignMessage implements TranslatableMessage { * The string displayed when a sign property is denied */ SIGN_PROPERTY_DENY, + + /** + * The message displayed when a player command is used from the console + */ + ERROR_PLAYER_ONLY, + + /** + * The message to display when the player is not looking at a sign, which is required + */ + ERROR_NOT_LOOKING_AT_SIGN, + + /** + * The message displayed when ready to paste a sign + */ + SUCCESS_CLICK_SIGN_TO_PASTE, + + /** + * The message displayed when a sign is successfully pasted + */ + SUCCESS_SIGN_PASTED, + + /** + * The message displayed when a protection plugin cancels the sign change event + */ + ERROR_CANCELLED_BY_PROTECTION, ; @Override diff --git a/src/main/java/net/knarcraft/placeholdersigns/container/SignCopyRequest.java b/src/main/java/net/knarcraft/placeholdersigns/container/SignCopyRequest.java new file mode 100644 index 0000000..33ad28d --- /dev/null +++ b/src/main/java/net/knarcraft/placeholdersigns/container/SignCopyRequest.java @@ -0,0 +1,14 @@ +package net.knarcraft.placeholdersigns.container; + +import org.bukkit.block.Sign; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * A record of a player's request to copy a sign + * + * @param player

The player requesting the sign copy

+ * @param sign

The sign the player wants to copy

+ */ +public record SignCopyRequest(@NotNull Player player, @NotNull Sign sign) { +} diff --git a/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignRequestHandler.java b/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignRequestHandler.java index 083af40..cb4caae 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignRequestHandler.java +++ b/src/main/java/net/knarcraft/placeholdersigns/handler/PlaceholderSignRequestHandler.java @@ -1,5 +1,6 @@ package net.knarcraft.placeholdersigns.handler; +import net.knarcraft.placeholdersigns.container.SignCopyRequest; import net.knarcraft.placeholdersigns.container.SignLineChangeRequest; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -13,17 +14,38 @@ import java.util.Map; */ public class PlaceholderSignRequestHandler { - private final Map signChangeRequests; - private final Map signViewRequests; - private final Map placeholderSignViewRequests; + private final @NotNull Map signChangeRequests; + private final Map signCopyRequests; /** * Instantiates a new placeholder sign request handler */ public PlaceholderSignRequestHandler() { this.signChangeRequests = new HashMap<>(); - this.signViewRequests = new HashMap<>(); - this.placeholderSignViewRequests = new HashMap<>(); + this.signCopyRequests = new HashMap<>(); + } + + /** + * Registers a sign copy request + * + *

A sign copy request is basically the result of running the copySign command, which must be stored until the + * player clicks a sign.

+ * + * @param request

The sign copy request to register

+ */ + public void addSignCopyRequest(@NotNull SignCopyRequest request) { + this.signCopyRequests.put(request.player(), request); + } + + /** + * Gets a sign copy request + * + * @param player

The player to get the request for

+ * @return

The sign copy request, or null if not found

+ */ + @Nullable + public SignCopyRequest getSignCopyRequest(@NotNull Player player) { + return this.signCopyRequests.remove(player); } /** @@ -49,54 +71,4 @@ public class PlaceholderSignRequestHandler { return this.signChangeRequests.remove(player); } - /** - * Registers a sign view request - * - * @param player

The player requesting to view a sign

- * @param raw

Whether to display the raw text (visible formatting codes)

- */ - public void addSignViewRequest(@NotNull Player player, boolean raw) { - this.signViewRequests.put(player, raw); - } - - /** - * Checks whether the given player has a sign view request - * - * @param player

The player to check

- * @return

Not null if the player has requested to view a sign

- */ - @Nullable - public Boolean getSignViewRequest(@NotNull Player player) { - if (this.signViewRequests.containsKey(player)) { - return this.signViewRequests.remove(player); - } else { - return null; - } - } - - /** - * Registers a placeholder view request - * - * @param player

The player requesting to view a placeholder sign

- * @param raw

Whether to display the raw text (visible formatting codes)

- */ - public void addPlaceholderSignViewRequest(Player player, boolean raw) { - this.placeholderSignViewRequests.put(player, raw); - } - - /** - * Checks whether the given player has a placeholder sign view request - * - * @param player

The player to check

- * @return

Not null if the player has requested to view a sign

- */ - @Nullable - public Boolean getPlaceholderSignViewRequest(@NotNull Player player) { - if (this.placeholderSignViewRequests.containsKey(player)) { - return this.placeholderSignViewRequests.remove(player); - } else { - return null; - } - } - } diff --git a/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java b/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java index 9e1da49..545648a 100644 --- a/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java +++ b/src/main/java/net/knarcraft/placeholdersigns/listener/SignClickListener.java @@ -1,19 +1,16 @@ package net.knarcraft.placeholdersigns.listener; import net.knarcraft.knarlib.formatting.StringFormatter; -import net.knarcraft.knarlib.formatting.StringReplacer; import net.knarcraft.knarlib.property.ColorConversion; import net.knarcraft.knarlib.util.ColorHelper; import net.knarcraft.placeholdersigns.PlaceholderSigns; import net.knarcraft.placeholdersigns.config.PlaceholderSignMessage; import net.knarcraft.placeholdersigns.container.PlaceholderSign; +import net.knarcraft.placeholdersigns.container.SignCopyRequest; import net.knarcraft.placeholdersigns.container.SignLineChangeRequest; import net.knarcraft.placeholdersigns.handler.PlaceholderSignHandler; import net.knarcraft.placeholdersigns.handler.PlaceholderSignRequestHandler; -import net.md_5.bungee.api.ChatColor; import org.bukkit.Bukkit; -import org.bukkit.DyeColor; -import org.bukkit.Location; import org.bukkit.block.Sign; import org.bukkit.block.sign.Side; import org.bukkit.block.sign.SignSide; @@ -24,6 +21,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.SignChangeEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.Objects; @@ -44,17 +42,13 @@ public class SignClickListener implements Listener { Player player = event.getPlayer(); PlaceholderSignRequestHandler requestHandler = PlaceholderSigns.getInstance().getRequestHandler(); - Boolean hasSignViewRequest = requestHandler.getSignViewRequest(player); - if (hasSignViewRequest != null) { - printSign(sign, player, hasSignViewRequest, false); - // Cancel the event to prevent vanilla behavior - event.setCancelled(true); - return; - } + SignCopyRequest signCopyRequest = requestHandler.getSignCopyRequest(player); + if (signCopyRequest != null) { + if (checkWaxEdit(sign, player, "placeholdersigns.copy.bypass-waxed")) { + return; + } - Boolean hasPlaceholderSignViewRequest = requestHandler.getPlaceholderSignViewRequest(player); - if (hasPlaceholderSignViewRequest != null) { - printSign(sign, player, hasPlaceholderSignViewRequest, true); + pasteSign(signCopyRequest.sign(), sign, player); event.setCancelled(true); return; } @@ -62,9 +56,7 @@ public class SignClickListener implements Listener { // Check if the player has run the /editSign command SignLineChangeRequest request = requestHandler.getSignChangeRequest(player); if (request != null) { - if (sign.isWaxed() && !player.hasPermission("placeholdersigns.edit.bypass-waxed")) { - PlaceholderSigns.getInstance().getStringFormatter().displayErrorMessage(player, - PlaceholderSignMessage.ERROR_WAXED_NO_PERMISSION); + if (checkWaxEdit(sign, player, "placeholdersigns.edit.bypass-waxed")) { return; } @@ -75,114 +67,138 @@ public class SignClickListener implements Listener { } /** - * Prints the current contents of a sign to a player + * Checks if the player is trying to edit/modify a waxed sign without permission * - * @param sign

The sign to print

- * @param player

The player to display the contents to

- * @param raw

Whether to get the raw text with used formatting codes

- * @param showPlaceholders

Whether to show the actual placeholders stored on the sign

+ * @param sign

The sign the player is trying to edit

+ * @param player

The player trying to edit the sign

+ * @return

True if the player is trying to edit a waxed sign without permission

*/ - private void printSign(@NotNull Sign sign, @NotNull Player player, boolean raw, boolean showPlaceholders) { - Location location = sign.getLocation(); - SignSide front = sign.getSide(Side.FRONT); - SignSide back = sign.getSide(Side.BACK); - String frontLines = showPlaceholders ? getPlaceholderSignText(location, front, Side.FRONT, raw) : - getSignText(front.getLines(), raw); - String backLines = showPlaceholders ? getPlaceholderSignText(location, back, Side.BACK, raw) : - getSignText(back.getLines(), raw); - - StringFormatter stringFormatter = PlaceholderSigns.getInstance().getStringFormatter(); - StringReplacer replacer = new StringReplacer(stringFormatter.getUnformattedColoredMessage( - PlaceholderSignMessage.SUCCESS_SIGN_CONTENTS)); - replacer.add("{frontLines}", frontLines); - replacer.add("{backLines}", backLines); - replacer.add("{frontDye}", getDye(front)); - replacer.add("{frontGlow}", getStatus(front.isGlowingText())); - replacer.add("{backDye}", getDye(back)); - replacer.add("{backGlow}", getStatus(back.isGlowingText())); - replacer.add("{waxed}", getStatus(sign.isWaxed())); - player.sendMessage(replacer.replace()); - } - - /** - * Gets a description of the status of a sign property - * - * @param isTrue

Whether the property is true

- * @return

A description of the property's current status

- */ - @NotNull - private String getStatus(boolean isTrue) { - StringFormatter stringFormatter = PlaceholderSigns.getInstance().getStringFormatter(); - if (isTrue) { - return stringFormatter.getUnformattedColoredMessage(PlaceholderSignMessage.SIGN_PROPERTY_CONFIRM); + private boolean checkWaxEdit(@NotNull Sign sign, @NotNull Player player, @NotNull String permissionNode) { + if (sign.isWaxed() && !player.hasPermission(permissionNode)) { + PlaceholderSigns.getInstance().getStringFormatter().displayErrorMessage(player, + PlaceholderSignMessage.ERROR_WAXED_NO_PERMISSION); + return true; } else { - return stringFormatter.getUnformattedColoredMessage(PlaceholderSignMessage.SIGN_PROPERTY_DENY); + return false; } } /** - * Gets a description of a dye applied to a sign + * Pastes the data from the source sign to the target sign * - * @param signSide

The sign side to get the dye of

- * @return

The description of the applied dye

+ * @param source

The source sign to copy data from

+ * @param target

The target sign to paste to

+ * @param player

The player pasting the sign

*/ - @NotNull - private String getDye(@NotNull SignSide signSide) { - DyeColor dyeColor = signSide.getColor(); - if (dyeColor == null) { - return "None"; + private void pasteSign(@NotNull Sign source, @NotNull Sign target, @NotNull Player player) { + StringFormatter stringFormatter = PlaceholderSigns.getInstance().getStringFormatter(); + PlaceholderSign placeholderSign = PlaceholderSigns.getInstance().getSignHandler().getFromLocation(source.getLocation()); + SignSide sourceFront = source.getSide(Side.FRONT); + SignSide sourceBack = source.getSide(Side.BACK); + SignSide targetFront = target.getSide(Side.FRONT); + SignSide targetBack = target.getSide(Side.BACK); + + // Get the lines, with placeholders replaced, and the sign change event processed + String[] frontLines = getFinalLines(target, sourceFront, Side.FRONT, placeholderSign, player); + if (frontLines == null) { + stringFormatter.displayErrorMessage(player, PlaceholderSignMessage.ERROR_CANCELLED_BY_PROTECTION); + return; + } + String[] backLines = getFinalLines(target, sourceBack, Side.BACK, placeholderSign, player); + if (backLines == null) { + stringFormatter.displayErrorMessage(player, PlaceholderSignMessage.ERROR_CANCELLED_BY_PROTECTION); + return; } - ChatColor color = ColorHelper.fromColor(dyeColor.getColor()); - return color + signSide.getColor().name(); + // Update the lines on both sides of the target sign + for (int i = 0; i < frontLines.length; i++) { + targetFront.setLine(i, ColorHelper.translateColorCodes(frontLines[i], ColorConversion.RGB)); + } + for (int i = 0; i < backLines.length; i++) { + targetBack.setLine(i, ColorHelper.translateColorCodes(backLines[i], ColorConversion.RGB)); + } + + // Copy options + copySignOptions(source, target); + + // Apply changes + target.update(); + + stringFormatter.displaySuccessMessage(player, PlaceholderSignMessage.SUCCESS_SIGN_PASTED); } /** - * Gets text from a sign side, and appends it to the given string builder + * Gets the final lines from a sign side, after inserting placeholders and running a sign change event * - * @param signLocation

The location of the sign

- * @param signSide

The sign side to get text from

- * @param side

The side of the sign to get placeholders from

- * @param raw

Whether to get the raw text with used formatting codes

+ * @param sign

The sign that's changed

+ * @param signSide

The side of the sign to get lines from

+ * @param side

The side that's processed

+ * @param placeholderSign

The placeholder sign corresponding to the sign, if any

+ * @param player

The player attempting to paste the sign

+ * @return

The final lines, or null if the event was cancelled

*/ - @NotNull - private String getPlaceholderSignText(@NotNull Location signLocation, @NotNull SignSide signSide, - @NotNull Side side, boolean raw) { - String[] lines = signSide.getLines(); - PlaceholderSign placeholderSign = PlaceholderSigns.getInstance().getSignHandler().getFromLocation(signLocation); - if (placeholderSign == null) { - return getSignText(lines, raw); + @Nullable + private String[] getFinalLines(@NotNull Sign sign, @NotNull SignSide signSide, @NotNull Side side, + @Nullable PlaceholderSign placeholderSign, @NotNull Player player) { + String[] sideLines = signSide.getLines(); + if (placeholderSign != null) { + Map> placeholders = placeholderSign.placeholders(); + loadPlaceholders(side, sideLines, placeholders); } - Map placeholders = placeholderSign.placeholders().get(side); - for (Map.Entry entry : placeholders.entrySet()) { - lines[entry.getKey()] = entry.getValue(); + // Run the sign change event to allow protection plugins to cancel, and allow the sign text listener to trigger + SignChangeEvent changeEvent = new SignChangeEvent(Objects.requireNonNull(sign.getBlock()), player, sideLines, side); + Bukkit.getPluginManager().callEvent(changeEvent); + if (changeEvent.isCancelled()) { + return null; } - return getSignText(lines, raw); + return changeEvent.getLines(); } /** - * Gets text from a sign side, and appends it to the given string builder + * Copies all sign option (dye, glowing, waxed) from the source to the target * - * @param lines

The lines on the sign

- * @param raw

Whether to get the raw text with used formatting codes

+ * @param source

The source sign

+ * @param target

The target sign

*/ - @NotNull - private String getSignText(@NotNull String[] lines, boolean raw) { - StringBuilder output = new StringBuilder(); + private void copySignOptions(@NotNull Sign source, @NotNull Sign target) { + SignSide sourceFront = source.getSide(Side.FRONT); + SignSide sourceBack = source.getSide(Side.BACK); + SignSide targetFront = target.getSide(Side.FRONT); + SignSide targetBack = target.getSide(Side.BACK); + + // Copy glowing state + targetFront.setGlowingText(sourceFront.isGlowingText()); + targetBack.setGlowingText(sourceBack.isGlowingText()); + + // Copy applied dye + targetFront.setColor(sourceFront.getColor()); + targetBack.setColor(sourceBack.getColor()); + + // Apply waxed state + target.setWaxed(source.isWaxed()); + } + + /** + * Loads placeholders from one side of a sign + * + * @param side

The side to load from

+ * @param lines

The array to write placeholders to

+ * @param placeholders

The placeholders to load

+ */ + private void loadPlaceholders(@NotNull Side side, @NotNull String[] lines, + @NotNull Map> placeholders) { + if (!placeholders.containsKey(side)) { + return; + } + + Map sidePlaceholders = placeholders.get(side); for (int i = 0; i < 4; i++) { - output.append(i + 1).append(". "); - - String line = lines[i]; - if (raw) { - output.append(line.replace(ChatColor.COLOR_CHAR, '&')); - } else { - output.append(line); + if (sidePlaceholders.containsKey(i)) { + lines[i] = sidePlaceholders.get(i); } - output.append(ChatColor.COLOR_CHAR).append("r\n"); } - return output.toString(); } /** @@ -220,6 +236,8 @@ public class SignClickListener implements Listener { // Restore the old placeholder if the action didn't complete placeholderSign.placeholders().get(side).put(request.line(), oldPlaceholder); } + PlaceholderSigns.getInstance().getStringFormatter().displayErrorMessage(player, + PlaceholderSignMessage.ERROR_CANCELLED_BY_PROTECTION); return; } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index e828c63..1509941 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -6,33 +6,27 @@ depend: - PlaceholderAPI commands: - editSign: + setSignLine: usage: / [text] [text] ... - permission: placeholdersigns.edit + permission: placeholdersigns.edit.use description: Changes the line of a sign, without a text limit, and with color conversion viewSign: - usage: / + usage: / [output raw formatting codes (true/false)] [show placeholders (true/false)] permission: placeholdersigns.view description: Displays the contents of a sign in the chat (useful for lines exceeding the viewable area) - viewSignRaw: + copySign: usage: / - permission: placeholdersigns.view - description: Displays the raw contents of a sign in the chat (useful for lines exceeding the viewable area, or to see formatting used) - viewPlaceholderSign: - usage: / - permission: placeholdersigns.view - description: Displays the contents of a sign in the chat, including stored placeholders - viewPlaceholderSignRaw: - usage: / - permission: placeholdersigns.view - description: Displays the raw contents of a sign in the chat, including stored placeholders + permission: placeholdersigns.copy.use + description: Copies all sign information from one sign to another permissions: placeholdersigns.*: + description: Grants all placeholdersigns permissions children: - placeholdersigns.edit - placeholdersigns.placeholder - placeholdersigns.view + - placeholdersigns.copy default: op placeholdersigns.edit: description: Allows a player to use the /editSign command without restriction @@ -40,6 +34,12 @@ permissions: children: - placeholdersigns.edit.use - placeholdersigns.edit.bypass-waxed + placeholdersigns.copy: + description: Allows a player to copy a sign to another sign + default: false + children: + - placeholdersigns.copy.use + - placeholdersigns.copy.bypass-waxed placeholdersigns.edit.use: description: Allows a player to use the /editSign command default: false @@ -51,4 +51,10 @@ permissions: default: false placeholdersigns.view: description: Allows a player to see the full text of a sign in the chat - default: true \ No newline at end of file + default: true + placeholdersigns.copy.use: + description: Allows a player to copy a sign to another sign + default: false + placeholdersigns.copy.bypass-waxed: + description: Allows a player to use the /copySign command and paste onto a waxed sign + default: false \ No newline at end of file diff --git a/src/main/resources/strings.yml b/src/main/resources/strings.yml index f7d5d16..7d5eff5 100644 --- a/src/main/resources/strings.yml +++ b/src/main/resources/strings.yml @@ -1,8 +1,7 @@ en: - SUCCESS_CLICK_SIGN_TO_EDIT: "Please click the sign you want to change." - SUCCESS_CLICK_SIGN_TO_VIEW: "Please click the sign you want to view." + SUCCESS_CLICK_SIGN_TO_EDIT: "&7Please click the sign you want to change." ERROR_WAXED_NO_PERMISSION: "You do not have the necessary permissions to edit a waxed sign." - SUCCESS_SIGN_CHANGED: "The sign line was successfully changed." + SUCCESS_SIGN_CHANGED: "&7The sign line was successfully changed." SUCCESS_SIGN_CONTENTS: | #78da55&lSign contents:&r #17A057Front:&r @@ -13,4 +12,9 @@ en: #ffdf32Waxed {waxed} SIGN_PROPERTY_CONFIRM: "#FDFFC8Yes" - SIGN_PROPERTY_DENY: "#CAC8FFNo" \ No newline at end of file + SIGN_PROPERTY_DENY: "#CAC8FFNo" + ERROR_PLAYER_ONLY: "This command must be used by a player" + ERROR_NOT_LOOKING_AT_SIGN: "You are not currently looking at a sign" + 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" \ No newline at end of file