package me.desht.scrollingmenusign;
import me.desht.dhutils.ItemGlow;
import me.desht.dhutils.LogUtils;
import me.desht.dhutils.MiscUtil;
import me.desht.dhutils.PermissionUtils;
import me.desht.scrollingmenusign.enums.ReturnStatus;
import me.desht.scrollingmenusign.parser.CommandUtils;
import me.desht.scrollingmenusign.util.SMSUtil;
import me.desht.scrollingmenusign.views.CommandTrigger;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.Validate;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.material.MaterialData;
import java.util.*;
public class SMSMenuItem implements Comparable<SMSMenuItem>, SMSUseLimitable {
private final String label;
private final String command;
private final String message;
private final List<String> lore;
private final ItemStack icon;
private final String altCommand;
private final String permissionNode;
private SMSRemainingUses uses;
private final SMSMenu menu;
public SMSMenuItem(SMSMenu menu, String label, String command, String message) {
this(menu, label, command, message, null);
}
public SMSMenuItem(SMSMenu menu, String label, String command, String message, String iconMaterialName) {
this(menu, label, command, message, iconMaterialName, new String[0]);
}
public SMSMenuItem(SMSMenu menu, String label, String command, String message, String iconMaterialName, String[] lore) {
Validate.notNull(menu, "menu may not be null");
Validate.notNull(label, "label may not be null");
Validate.notNull(command, "command may not be null");
this.menu = menu;
this.label = label;
this.command = command;
this.altCommand = "";
this.message = message;
this.permissionNode = "";
try {
this.icon = SMSUtil.parseMaterialSpec(iconMaterialName);
} catch (IllegalArgumentException e) {
throw new SMSException("invalid material '" + iconMaterialName + "'");
}
this.lore = new ArrayList<String>();
for (String l : lore) {
this.lore.add(MiscUtil.parseColourSpec(l));
}
this.uses = new SMSRemainingUses(this);
}
public SMSMenuItem(SMSMenu menu, ConfigurationSection node) throws SMSException {
SMSPersistence.mustHaveField(node, "label");
this.menu = menu;
this.label = SMSUtil.unEscape(node.getString("label"));
this.command = StringEscapeUtils.unescapeHtml(node.getString("command", ""));
this.altCommand = StringEscapeUtils.unescapeHtml(node.getString("altCommand", ""));
this.message = SMSUtil.unEscape(node.getString("message", ""));
this.icon = SMSUtil.parseMaterialSpec(node.getString("icon"));
this.uses = new SMSRemainingUses(this, node.getConfigurationSection("usesRemaining"));
this.lore = new ArrayList<String>();
this.permissionNode = node.getString("permission", "");
if (node.contains("lore")) {
for (String l : node.getStringList("lore")) {
lore.add(SMSUtil.unEscape(l));
}
}
}
private SMSMenuItem(Builder builder) {
this.menu = builder.menu;
this.label = builder.label;
this.message = builder.message;
this.lore = builder.lore;
this.command = builder.command;
this.altCommand = builder.altCommand;
this.permissionNode = builder.permissionNode;
this.icon = builder.icon;
if (builder.glow && builder.icon != null && ScrollingMenuSign.getInstance().isProtocolLibEnabled()) {
ItemGlow.setGlowing(this.icon, true);
}
this.uses = builder.uses == null ? new SMSRemainingUses(this) : builder.uses;
}
Map<String, Object> freeze() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("label", SMSUtil.escape(label));
if (!command.isEmpty()) {
map.put("command", SMSUtil.escape(command));
}
if (!altCommand.isEmpty()) {
map.put("altCommand", SMSUtil.escape(altCommand));
}
if (!message.isEmpty()) {
map.put("message", SMSUtil.escape(message));
}
if (hasIcon()) {
map.put("icon", SMSUtil.freezeMaterialSpec(getIcon()));
}
if (uses.hasLimitedUses()) {
map.put("usesRemaining", uses.freeze());
}
if (!lore.isEmpty()) {
List<String> lore2 = new ArrayList<String>(lore.size());
for (String l : lore) {
lore2.add(SMSUtil.escape(l));
}
map.put("lore", lore2);
}
if (!permissionNode.isEmpty()) {
map.put("permission", permissionNode);
}
return map;
}
/**
* Get a string representation of the icon's material name.
*
* @return the icon material & data as a string
* @deprecated call {@code toString()} on {@link #getIconMaterial()} if you really need this
*/
@Deprecated
public String getIconMaterialName() {
return getIconMaterial() == null ? null : getIconMaterial().toString();
}
/**
* Get the label for this menu item
*
* @return The label
*/
public String getLabel() {
return label;
}
/**
* Get the label for this menu item with all colour codes removed
*
* @return The label
*/
public String getLabelStripped() {
return ChatColor.stripColor(label);
}
/**
* Get the command for this menu item
*
* @return The command
*/
public String getCommand() {
return command;
}
/**
* Get the alternative (secondary) command for this menu item
*
* @return the alternative command
*/
public String getAltCommand() {
return altCommand;
}
/**
* Get the feedback message for this menu item
*
* @return The feedback message
*/
public String getMessage() {
return message;
}
/**
* Return the material used for this menu item's icon, in those views which
* support icons.
*
* @return the material used for the menu item's icon
*/
public MaterialData getIconMaterial() {
return hasIcon() ? icon.getData() : null;
}
/**
* Check if this menu item has an icon defined.
*
* @return true if the item has an icon, false otherwise
*/
public boolean hasIcon() {
return icon != null;
}
/**
* Get the item's icon as an item stack.
*
* @return a copy of the itme's icon
*/
public ItemStack getIcon() {
return icon == null ? null : icon.clone();
}
/**
* Get the lore (tooltip) for this menu item. Note that not all view types necessarily support
* display of lore.
*
* @return the lore for the menu item, as a String array
*/
public String[] getLore() {
return lore.toArray(new String[lore.size()]);
}
/**
* Get the lore (tooltip) for this menu item. Note that not all view types necessarily support
* display of lore.
*
* @return the lore for the menu item, as a list of String
*/
public List<String> getLoreAsList() {
return new ArrayList<String>(lore);
}
/**
* Append a line of text to the item's lore.
*
* @param lore the lore text to append
*/
public void appendLore(String lore) {
this.lore.add(lore);
}
/**
* Replace the item's lore with a line of text.
*
* @param lore the new lore text for the item
*/
public void setLore(String lore) {
this.lore.clear();
this.lore.add(lore);
}
/**
* Get the item's permission node, if any.
*
* @return the item's permission node, may be an empty string
*/
public String getPermissionNode() {
return permissionNode;
}
/**
* Executes the command for this item
*
* @param sender the command sender who triggered the execution
* @param trigger the view that triggered this execution
* @throws SMSException if the usage limit for this player is exhausted
*/
public void executeCommand(CommandSender sender, CommandTrigger trigger) {
executeCommand(sender, trigger, false);
}
/**
* Executes the command for this item
*
* @param sender the command sender who triggered the execution
* @param trigger the view that triggered this execution
* @param alt true if the item's alternate command should be executed
* @throws SMSException if the usage limit for this player is exhausted
*/
public void executeCommand(CommandSender sender, CommandTrigger trigger, boolean alt) {
SMSValidate.isTrue(hasPermission(sender), "That item is not available to you.");
String cmd = getCommand();
if (alt && !getAltCommand().isEmpty()) {
cmd = getAltCommand();
}
boolean itemUses = false, menuUses = false;
if (sender instanceof Player) {
itemUses = verifyRemainingUses(this, (Player) sender);
menuUses = verifyRemainingUses(menu, (Player) sender);
}
if ((cmd == null || cmd.isEmpty()) && !menu.getDefaultCommand().isEmpty()) {
cmd = menu.getDefaultCommand().replace("<LABEL>", ChatColor.stripColor(getLabel())).replace("<RAWLABEL>", getLabel());
}
CommandUtils.executeCommand(sender, cmd, trigger);
ReturnStatus rs = CommandUtils.getLastReturnStatus();
if (rs == ReturnStatus.CMD_OK || rs == ReturnStatus.UNKNOWN) {
if (itemUses) {
decrementRemainingUses(this, (Player) sender);
}
if (menuUses) {
decrementRemainingUses(menu, (Player) sender);
}
if (itemUses || menuUses) {
menu.autosave();
}
}
}
/**
* Executes the command for this item
*
* @param sender the command sender who triggered the execution
* @throws SMSException if the usage limit for this player is exhausted
*/
public void executeCommand(CommandSender sender) {
executeCommand(sender, null);
}
/**
* Verify that the given object (item or menu) has not exhausted its usage limits.
*
* @param useLimitable the menu or item to check
* @param player the player to check
* @return true if there is a valid usage limit, false if the item has no usage limits at all
* @throws SMSException if the usage limits for the item were already exhausted
*/
private boolean verifyRemainingUses(SMSUseLimitable useLimitable, Player player) throws SMSException {
SMSRemainingUses limits = useLimitable.getUseLimits();
if (limits.hasLimitedUses()) {
String desc = limits.getDescription();
if (limits.getRemainingUses(player) <= 0) {
throw new SMSException("You can't use that " + desc + " anymore.");
}
return true;
} else {
return false;
}
}
private void decrementRemainingUses(SMSUseLimitable useLimitable, Player player) {
SMSRemainingUses limits = useLimitable.getUseLimits();
if (limits.hasLimitedUses()) {
String desc = limits.getDescription();
limits.use(player);
if ((Boolean) menu.getAttributes().get(SMSMenu.REPORT_USES)) {
MiscUtil.statusMessage(player, "&6&o[Uses remaining for this " + desc + ": &e&o" + limits.getRemainingUses(player) + "&6&o]");
}
}
}
/**
* Displays the feedback message for this menu item
*
* @param player Player to show the message to
*/
public void feedbackMessage(Player player) {
if (player != null) {
sendFeedback(player, getMessage());
}
}
private void sendFeedback(Player player, String message) {
sendFeedback(player, message, new HashSet<String>());
}
private void sendFeedback(Player player, String message, Set<String> history) {
if (message == null || message.length() == 0)
return;
if (message.startsWith("%")) {
// macro expansion
String macro = message.substring(1);
if (history.contains(macro)) {
LogUtils.warning("Recursive loop detected in macro [" + macro + "]!");
throw new SMSException("Recursive loop detected in macro [" + macro + "]!");
} else if (SMSMacro.hasMacro(macro)) {
history.add(macro);
sendFeedback(player, SMSMacro.getCommands(macro), history);
} else {
throw new SMSException("No such macro [" + macro + "].");
}
} else {
MiscUtil.alertMessage(player, message);
}
}
private void sendFeedback(Player player, List<String> messages, Set<String> history) {
for (String m : messages) {
sendFeedback(player, m, history);
}
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "SMSMenuItem [label=" + label + ", command=" + command + ", message=" + message + ", icon=" + icon + "]";
}
/**
* Get the remaining use details for this menu item
*
* @return The remaining use details
*/
@Override
public SMSRemainingUses getUseLimits() {
return uses;
}
@Override
public String getLimitableName() {
return menu.getName() + "/" + getLabelStripped();
}
/**
* Sets the remaining use details for this menu item.
*
* @param uses the remaining use details
*/
public void setUseLimits(SMSRemainingUses uses) {
this.uses = uses;
}
/**
* Returns a printable representation of the number of uses remaining for this item.
*
* @return Formatted usage information
*/
String formatUses() {
return uses.toString();
}
/**
* Returns a printable representation of the number of uses remaining for this item, for the given player.
*
* @param sender Player to retrieve the usage information for
* @return Formatted usage information
*/
@Override
public String formatUses(CommandSender sender) {
if (sender instanceof Player) {
return uses.toString((Player) sender);
} else {
return formatUses();
}
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((command == null) ? 0 : command.hashCode());
result = prime * result + ((label == null) ? 0 : label.hashCode());
result = prime * result + ((message == null) ? 0 : message.hashCode());
return result;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SMSMenuItem other = (SMSMenuItem) obj;
if (command == null) {
if (other.command != null)
return false;
} else if (!command.equals(other.command))
return false;
if (label == null) {
if (other.label != null)
return false;
} else if (!label.equals(other.label))
return false;
if (message == null) {
if (other.message != null)
return false;
} else if (!message.equals(other.message))
return false;
return true;
}
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*
* Two menu items are equal if their labels are the same. Colour codes do not count, only the text.
*/
@Override
public int compareTo(SMSMenuItem other) {
return getLabelStripped().compareToIgnoreCase(other.getLabelStripped());
}
public void autosave() {
if (menu != null)
menu.autosave();
}
@Override
public String getDescription() {
return "menu item";
}
SMSMenuItem uniqueItem() {
if (menu.getItem(getLabelStripped()) == null) {
return this;
}
// the label already exists in this menu - try to get a unique one
int n = 0;
String ls;
do {
n++;
ls = getLabelStripped() + "-" + n;
} while (menu.getItem(ls) != null);
return new SMSMenuItem.Builder(menu, label)
.withCommand(getCommand())
.withMessage(getMessage())
.withIcon(getIcon())
.withAltCommand(getAltCommand())
.withLore(getLore())
.withUseLimits(getUseLimits())
.withPermissionNode(getPermissionNode())
.build();
}
/**
* Check if the given player has permission to use this specific menu item,
* as defined by its permission node (see {@link #getPermissionNode()}).
* Note that this only gives the player permission to execute the menu item,
* not necessarily permission for any command that may be run.
*
* @param sender the player to check
* @return true if the player may use this item, false otherwise
*/
public boolean hasPermission(CommandSender sender) {
return sender == null || permissionNode.isEmpty() || PermissionUtils.isAllowedTo(sender, permissionNode);
}
public SMSMenu getMenu() {
return menu;
}
public static class Builder {
private final SMSMenu menu;
private final String label;
private String command = "";
private String altCommand = "";
private String message = "";
private List<String> lore = Collections.emptyList();
private ItemStack icon;
private boolean glow;
private String permissionNode = "";
private SMSRemainingUses uses;
public Builder(SMSMenu menu, String label) {
this.menu = menu;
this.label = label;
}
public Builder withCommand(String command) {
this.command = command;
return this;
}
public Builder withMessage(String message) {
this.message = message;
return this;
}
public Builder withAltCommand(String altCommand) {
this.altCommand = altCommand;
return this;
}
public Builder withLore(String... lore) {
this.lore = Arrays.asList(lore);
return this;
}
public Builder withLore(List<String> lore) {
this.lore = new ArrayList<String>(lore);
return this;
}
public Builder withIcon(MaterialData icon) {
this.icon = icon.toItemStack();
return this;
}
public Builder withIcon(ItemStack icon) {
this.icon = icon;
return this;
}
public Builder withIcon(String iconMaterialName) {
try {
this.icon = SMSUtil.parseMaterialSpec(iconMaterialName);
} catch (IllegalArgumentException e) {
throw new SMSException("invalid material '" + iconMaterialName + "'");
}
return this;
}
public Builder withGlow(boolean glow) {
this.glow = glow;
return this;
}
public Builder withPermissionNode(String permissionNode) {
this.permissionNode = permissionNode;
return this;
}
public Builder withUseLimits(SMSRemainingUses uses) {
this.uses = uses;
return this;
}
public SMSMenuItem build() {
return new SMSMenuItem(this);
}
}
}