/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.shininet.bukkit.itemrenamer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import javax.annotation.Nullable;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.shininet.bukkit.itemrenamer.configuration.ConfigParsers;
import org.shininet.bukkit.itemrenamer.configuration.DamageLookup;
import org.shininet.bukkit.itemrenamer.configuration.DamageValues;
import org.shininet.bukkit.itemrenamer.configuration.ItemRenamerConfiguration;
import org.shininet.bukkit.itemrenamer.configuration.RenameProcessorFactory;
import org.shininet.bukkit.itemrenamer.configuration.RenameRule;
import org.shininet.bukkit.itemrenamer.serialization.DamageSerializer;
import org.shininet.bukkit.itemrenamer.utils.MaterialUtils;
import org.shininet.bukkit.itemrenamer.wrappers.LeveledEnchantment;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Ranges;
class ItemRenamerCommands implements CommandExecutor {
// Different permissions
private static final String PERM_GET = "itemrenamer.config.get";
private static final String PERM_SET= "itemrenamer.config.set";
// The super command
private static final Object COMMAND_ITEMRENAMER = "ItemRenamer";
// Selected items
private SelectedItemTracker selectedTracker;
private SelectedPackTracker selectedPacks;
// Recognized sub-commands
public enum Commands {
GET_AUTO_UPDATE,
SET_AUTO_UPDATE,
SET_DEFAULT_PACK,
GET_DEFAULT_PACK,
GET_WORLD_PACK,
SET_WORLD_PACK,
GET_ITEM,
GET_SELECTED,
ADD_PACK,
DELETE_PACK,
SELECT_PACK,
SELECT_HAND,
SELECT_NONE,
SET_NAME,
ADD_LORE,
DELETE_LORE,
ADD_ENCHANTMENT,
ADD_DECHANTMENT,
DELETE_ENCHANTMENTS,
DELETE_DECHANTMENTS,
RELOAD,
SAVE,
PAGE,
}
private final ItemRenamerPlugin plugin;
private final ItemRenamerConfiguration config;
private final CommandMatcher<Commands> matcher;
// Paged output
private final PagedMessage pagedMessage = new PagedMessage();
public ItemRenamerCommands(ItemRenamerPlugin plugin, ItemRenamerConfiguration config, SelectedItemTracker selectedTracker) {
this.plugin = plugin;
this.matcher = registerCommands();
this.config = config;
this.selectedTracker = selectedTracker;
this.selectedPacks = new SelectedPackTracker();
}
/**
* Retrieve the tracker for selected packs.
* @return Tracker for selected packs.
*/
public SelectedPackTracker getSelectedPacks() {
return selectedPacks;
}
/**
* Retrieve the tracker for selected items.
* @return Tracker for selected items.
*/
public SelectedItemTracker getSelectedTracker() {
return selectedTracker;
}
private CommandMatcher<Commands> registerCommands() {
CommandMatcher<Commands> output = new CommandMatcher<Commands>();
output.registerCommand(Commands.GET_AUTO_UPDATE, PERM_GET, "get", "setting", "autoupdate");
output.registerCommand(Commands.SET_AUTO_UPDATE, PERM_SET, "set", "setting", "autoupdate");
output.registerCommand(Commands.GET_WORLD_PACK, PERM_GET, "get", "world");
output.registerCommand(Commands.SET_WORLD_PACK, PERM_SET, "set", "world");
output.registerCommand(Commands.GET_DEFAULT_PACK, PERM_GET, "get", "default");
output.registerCommand(Commands.SET_DEFAULT_PACK, PERM_SET, "set", "default");
output.registerCommand(Commands.ADD_PACK, PERM_SET, "add", "pack");
output.registerCommand(Commands.GET_SELECTED, PERM_GET, "get", "selected");
output.registerCommand(Commands.DELETE_PACK, PERM_SET, "delete", "pack");
output.registerCommand(Commands.SELECT_PACK, PERM_SET, "select", "pack");
output.registerCommand(Commands.SELECT_HAND, PERM_SET, "select", "hand");
output.registerCommand(Commands.SELECT_NONE, PERM_SET, "select", "none");
output.registerCommand(Commands.GET_ITEM, PERM_GET, "get", "item");
output.registerCommand(Commands.SET_NAME, PERM_SET, "set", "name");
output.registerCommand(Commands.ADD_LORE, PERM_SET, "add", "lore");
output.registerCommand(Commands.DELETE_LORE, PERM_SET, "delete", "lore");
output.registerCommand(Commands.ADD_ENCHANTMENT, PERM_SET, "add", "enchantment");
output.registerCommand(Commands.ADD_DECHANTMENT, PERM_SET, "add", "dechantment");
output.registerCommand(Commands.DELETE_ENCHANTMENTS, PERM_SET, "delete", "enchantments");
output.registerCommand(Commands.DELETE_DECHANTMENTS, PERM_SET, "delete", "dechantments");
output.registerCommand(Commands.RELOAD, PERM_SET, "reload");
output.registerCommand(Commands.SAVE, PERM_SET, "save");
output.registerCommand(Commands.PAGE, null, "page");
return output;
}
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] arguments) {
if (cmd.getName().equals(COMMAND_ITEMRENAMER)) {
LinkedList<String> input = Lists.newLinkedList(Arrays.asList(arguments));
// See which node is closest
CommandMatcher<Commands>.CommandNode node = matcher.matchClosest(input);
if (node.isCommand()) {
if (node.getPermission() != null && !sender.hasPermission(node.getPermission())) {
sender.sendMessage(ChatColor.RED + "You need permission " + node.getPermission());
return true;
}
try {
String result = performCommand(sender, node.getCommand(), input);
if (result != null)
sender.sendMessage(ChatColor.GOLD + result);
} catch (CommandErrorException e) {
sender.sendMessage(ChatColor.RED + e.getMessage());
}
} else {
sender.sendMessage(ChatColor.RED + "Sub commands: " +
Joiner.on(", ").join(node.getChildren()));
}
// It's still somewhat correct
return true;
} else {
return false;
}
}
private String performCommand(CommandSender sender, Commands command, Deque<String> args) {
try {
switch (command) {
case GET_AUTO_UPDATE:
expectCommandCount(args, 0, "No arguments needed.");
return formatBoolean("Auto update is %s.", config.isAutoUpdate());
case SET_AUTO_UPDATE:
expectCommandCount(args, 1, "Need a yes/no argument.");
config.setAutoUpdate(parseBoolean(args.poll()));
return "Updated auto update.";
case GET_WORLD_PACK:
expectCommandCount(args, 1, "Need a world name.");
return getWorldPack(args);
case SET_WORLD_PACK:
expectCommandCount(args, 2, "Need a world name and a world pack name.");
return setWorldPack(args);
case GET_DEFAULT_PACK:
expectCommandCount(args, 0, "No arguments needed.");
return getDefaultPack();
case SET_DEFAULT_PACK:
expectCommandCount(args, 1, "Need a pack name.");
return setDefaultPack(args);
case ADD_PACK:
expectCommandCount(args, 1, "Need a world pack name.");
return addWorldPack(sender, args);
case DELETE_PACK:
expectCommandCount(args, 1, "Need a world pack name.");
return deleteWorldPack(args);
case SELECT_HAND:
expectCommandCount(args, 0, "No arguments needed.");
return selectCurrent(sender);
case SELECT_PACK:
expectCommandCount(args, 1, "Need a world pack name.");
return selectWorldPack(sender, args);
case SELECT_NONE:
String deselectedItem = deselectItemStack(sender);
return deselectWorldPack(sender) + (deselectedItem != null ? ". " + deselectedItem : "");
case GET_SELECTED:
return printSelected(sender);
case GET_ITEM:
return getItem(sender, args);
case SET_NAME:
return setItemName(sender, args);
case ADD_LORE:
return addLore(sender, args);
case DELETE_LORE:
return clearLore(sender, args);
case ADD_ENCHANTMENT:
return addEnchantment(sender, args);
case ADD_DECHANTMENT:
return addDechantment(sender, args);
case DELETE_ENCHANTMENTS:
return clearEnchantments(sender, args);
case DELETE_DECHANTMENTS:
return clearDechantment(sender, args);
case RELOAD:
config.reload();
return "Reloading configuration.";
case SAVE:
config.save();
return "Saving configuration to file.";
case PAGE:
List<Integer> pageNumber = ConfigParsers.getIntegers(args, 1, null);
if (pageNumber.size() == 1) {
return pagedMessage.getPage(sender, pageNumber.get(0));
} else {
throw new CommandErrorException("Must specify a page number.");
}
}
} catch (IllegalArgumentException e) {
throw new CommandErrorException(e.getMessage(), e);
}
throw new CommandErrorException("Unrecognized sub command: " + command);
}
/**
* Perform the "select hand" command. This will use the current items ID and durability,
* or the item itself.
* @param sender - the command sender.
* @return Result string.
*/
private String selectCurrent(CommandSender sender) {
// This will fail if the command sender is not a player
ItemStack previous = selectedTracker.selectCurrent(sender);
ItemStack current = selectedTracker.getItemToSelect(sender);
Player player = (Player) sender;
String worldName = player.getWorld().getName();
String worldPack = config.getEffectiveWorldPack(worldName);
// Select the current world too
if (!selectedPacks.hasSelected(sender)) {
if (worldPack != null)
selectedPacks.selectPack(sender, worldPack);
else
return "Please set the world pack first.";
}
// And we're done
if (previous != null) {
return "Selected " + current + " from " + previous;
} else {
return "Selected " + current;
}
}
/**
* Print the currently selected item and pack.
* @param sender - the command sender.
* @return The lines to print.
*/
private String printSelected(CommandSender sender) {
List<String> lines = new ArrayList<String>();
if (selectedPacks.hasSelected(sender))
lines.add("Selected pack " + selectedPacks.getSelected(sender));
if (selectedTracker.getSelected(sender) != null)
lines.add("Selected item " + selectedTracker.getSelected(sender));
if (lines.size() > 0) {
return Joiner.on("\n").join(lines);
} else {
return "Nothing selected.";
}
}
private String getItem(CommandSender sender, Deque<String> args) {
String pack = selectedPacks.getSelected(sender);
// Check for special selection
if (selectedTracker.hasExactSelector(sender) && pack != null) {
return "Rename: " + config.getRenameConfig().getExact(pack).
getRule(selectedTracker.getSelected(sender));
}
// Get all the arguments before we begin
final DamageLookup lookup = getLookup(sender, args);
if (args.isEmpty()) {
YamlConfiguration yaml = new YamlConfiguration();
DamageSerializer serializer = new DamageSerializer(yaml);
serializer.writeLookup(lookup);
// Display the lookup as a YAML
return pagedMessage.createPage(sender, yaml.saveToString());
}
final DamageValues damage = getDamageValues(sender, args);
if (damage == DamageValues.ALL)
return "Rename: " + lookup.getAllRule();
if (damage == DamageValues.ALL)
return "Rename: " + lookup.getAllRule();
else if (damage.getRange().lowerEndpoint().equals(damage.getRange().upperEndpoint()))
return "Rename: " + lookup.getDefinedRule(damage.getRange().lowerEndpoint());
else
throw new CommandErrorException("Cannot parse damage. Must be a single value, ALL or OTHER.");
}
private void modifyCurrent(CommandSender sender, Deque<String> args, boolean createNew, RenameProcessorFactory factory) {
String pack = selectedPacks.getSelected(sender);
// Exact matches
if (selectedTracker.hasExactSelector(sender) && pack != null) {
ItemStack stack = selectedTracker.getSelected(sender);
// Apply this to a new exact lookup
config.getRenameConfig().createExact(pack).
setTransform(stack, factory.create());
} else {
// Get all the arguments before we begin
DamageLookup lookup = createNew ? createLookup(sender, args) : getLookup(sender, args);
DamageValues damage = getDamageValues(sender, args);
if (lookup != null)
lookup.setTransform(damage, factory.create());
else
throw new IllegalArgumentException("No rename rule found.");
}
}
/**
* Retrieve the remaining arguments as a string.
* @param args - the arguments.
* @return The remaining arguments.
*/
private String getRemainder(Deque<String> args) {
return Joiner.on(" ").join(args);
}
private String setItemName(CommandSender sender, final Deque<String> args) {
modifyCurrent(sender, args, true, new RenameProcessorFactory() {
@Override
public RenameFunction create() {
final String name = getRemainder(args);
// This is Java alright ...
return new RenameFunction() {
@Override
public RenameRule apply(@Nullable RenameRule input) {
return RenameRule.newBuilder(input).name(name).build();
}
};
}
});
return String.format("Set the name of every item %s.", getRemainder(args));
}
private String addLore(CommandSender sender, final Deque<String> args) {
// Apply the change
modifyCurrent(sender, args, true, new RenameProcessorFactory() {
@Override
public RenameFunction create() {
final String lore = getRemainder(args);
return new RenameFunction() {
@Override
public RenameRule apply(@Nullable RenameRule input) {
return RenameRule.newBuilder(input).mergeLoreSections(Arrays.asList(lore)).build();
}
};
}
});
return String.format("Add the lore '%s' to every item.", getRemainder(args));
}
private String clearLore(CommandSender sender, Deque<String> args) {
// Use the clear attribute utility class
return clearAttribute(sender, args, new RenameOutputFactory() {
@Override
public RenameFunction create() {
return new RenameFunction() {
@Override
public RenameRule apply(@Nullable RenameRule input) {
output.append("Resetting lore for ").append(input);
return RenameRule.newBuilder(input).loreSections(null).build();
}
};
}
});
}
// The powerful programming technique of COPY PASTE
// I'm sorry.
private String clearEnchantments(CommandSender sender, Deque<String> args) {
return clearAttribute(sender, args, new RenameOutputFactory() {
@Override
public RenameFunction create() {
return new RenameFunction() {
@Override
public RenameRule apply(@Nullable RenameRule input) {
output.append("Resetting enchantments for ").append(input);
return RenameRule.newBuilder(input).enchantments(null).build();
}
};
}
});
}
private String clearDechantment(CommandSender sender, Deque<String> args) {
return clearAttribute(sender, args, new RenameOutputFactory() {
@Override
public RenameFunction create() {
return new RenameFunction() {
@Override
public RenameRule apply(@Nullable RenameRule input) {
output.append("Resetting dechantments for ").append(input);
return RenameRule.newBuilder(input).dechantments(null).build();
}
};
}
});
}
/**
* Modify the current item with the given output factory.
* @param sender - the command sender.
* @param args - the arguments.
* @param factory - the custom output factory.
* @return The result string, supplied by the output factory.
*/
private String clearAttribute(CommandSender sender, Deque<String> args, RenameOutputFactory factory) {
// Apply the change
modifyCurrent(sender, args, true, factory);
// Inform the user
if (factory.getOutput().length() == 0)
return "No items found.";
else
return factory.getOutput();
}
private String addEnchantment(CommandSender sender, final Deque<String> args) {
// Apply the change
modifyCurrent(sender, args, true, new RenameProcessorFactory() {
@Override
public RenameFunction create() {
final LeveledEnchantment enchantment = LeveledEnchantment.parse(args);
// Damn it
if (enchantment == null)
throw new IllegalArgumentException("Must specify an enchantment and level.");
return new RenameFunction() {
@Override
public RenameRule apply(@Nullable RenameRule input) {
return RenameRule.newBuilder(input).mergeEnchantments(Arrays.asList(enchantment)).build();
}
};
}
});
return String.format("Added enchantment to every selected item.");
}
private String addDechantment(CommandSender sender, final Deque<String> args) {
// Apply the change
modifyCurrent(sender, args, true, new RenameProcessorFactory() {
@Override
public RenameFunction create() {
final LeveledEnchantment enchantment = LeveledEnchantment.parse(args);
// Damn it
if (enchantment == null)
throw new IllegalArgumentException("Must specify an enchantment and level.");
return new RenameFunction() {
@Override
public RenameRule apply(@Nullable RenameRule input) {
return RenameRule.newBuilder(input).mergeDechantments(Arrays.asList(enchantment)).build();
}
};
}
});
return String.format("Dechanted every selected item.");
}
/**
* Retrieve the damage lookup based on the item pack and item ID in the parameter stack.
* @param sender - the original command sender.
* @param args - the parameter stack.
* @return The corresponding damage lookup.
*/
private DamageLookup getLookup(CommandSender sender, Deque<String> args) {
String pack = parsePack(sender, args);
Integer itemID = getItemID(args);
if (pack == null || pack.length() == 0)
throw new IllegalArgumentException("Must specify an item pack.");
return config.getRenameConfig().getLookup(pack, itemID);
}
private String parsePack(CommandSender sender, Deque<String> args) {
String selected = selectedPacks.getSelected(sender);
// Use the selected pack, or parse the input arguments
if (selected != null)
return selected;
else
return args.pollFirst();
}
private Integer getSelectedItemID(CommandSender sender, Deque<String> alternative) {
ItemStack stack = selectedTracker.getSelected(sender);
// Either use the selected stack, or look it up using the passed parameters
return stack != null ? stack.getTypeId() : getItemID(alternative);
}
private DamageLookup createLookup(CommandSender sender, Deque<String> args) {
String pack = parsePack(sender, args);
Integer itemID = getSelectedItemID(sender, args);
if (pack == null || pack.length() == 0)
throw new IllegalArgumentException("Must specify an item pack.");
return config.getRenameConfig().createLookup(pack, itemID);
}
private DamageValues getDamageValues(CommandSender sender, Deque<String> args) {
try {
ItemStack item = selectedTracker.getSelected(sender);
if (item != null) {
if (MaterialUtils.isArmorTool(item.getType()))
return DamageValues.OTHER;
else
return new DamageValues(item.getDurability());
} else {
return DamageValues.parse(args);
}
} catch (IllegalArgumentException e) {
// Wrap it in a more specific exception
throw new CommandErrorException(e.getMessage(), e);
}
}
private int getItemID(Deque<String> args) {
try {
List<Integer> result = ConfigParsers.getIntegers(args, 1, Ranges.closed(0, 4096));
if (result.size() == 1) {
return result.get(0);
} else {
throw new CommandErrorException("Cannot find item ID.");
}
} catch (IllegalArgumentException e) {
throw new CommandErrorException(e.getMessage(), e);
}
}
private String addWorldPack(CommandSender sender, Deque<String> args) {
String pack = args.poll();
if (config.getRenameConfig().createPack(pack))
return "Created pack " + pack;
else
return "Pack " + pack + " already exists";
}
private String selectWorldPack(CommandSender sender, Deque<String> args) {
String pack = args.poll();
// Either add or remove the selection
if (pack == null) {
String previous = selectedPacks.deselectPack(sender);
return previous != null ? "Deselected " + previous : "No pack selected.";
} if (config.getRenameConfig().hasPack(pack)) {
selectedPacks.selectPack(sender, pack);
return "Selected pack " + pack;
} else {
return "Pack " + pack + " doesn't exist.";
}
}
private String deleteWorldPack(Deque<String> args) {
String pack = args.poll();
config.getRenameConfig().removePack(pack);
return "Deleted pack " + pack;
}
private String deselectWorldPack(CommandSender sender) {
String removed = selectedPacks.deselectPack(sender);
if (removed != null)
return "Deselected pack " + removed;
else
return "No pack was selected.";
}
private String deselectItemStack(CommandSender sender) {
ItemStack removed = selectedTracker.deselectCurrent(sender);
if (removed != null)
return "Deselected item " + removed;
else
return null;
}
private void expectCommandCount(Deque<String> args, int expected, String error) {
if (expected != args.size())
throw new CommandErrorException("Error: " + error);
}
private String getDefaultPack() {
if (config.getDefaultPack() == null)
return "No default item pack.";
else
return "Default item pack: " + config.getDefaultPack();
}
private String getWorldPack(Deque<String> args) {
String world = args.poll();
// Retrieve world pack
return "Item pack for " + world + ": " + config.getWorldPack(world);
}
/**
* Set the default world pack.
* @param args - the input arguments.
* @return The message to return to the player.
*/
public String setDefaultPack(Deque<String> args) {
String pack = args.poll();;
config.setDefaultPack(pack);
return "Set the default item pack to " + pack;
}
/**
* Set the world pack we will use based on the input arguments.
* @param args - the input arguments.
* @return The message to return to the player.
*/
public String setWorldPack(Deque<String> args) {
String world = checkWorld(args.poll()), pack = args.poll();
config.setWorldPack(world, pack);
return "Set the item pack in world " + world + " to " + pack;
}
/**
* Determine if the given world actually exists.
* @param world - the world to test.
* @return The name of the world.
* @throws CommandErrorException If the world doesn't exist.
*/
private String checkWorld(String world) {
// Ensure the world exists
if (plugin.getServer().getWorld(world) == null)
throw new CommandErrorException("Cannot find world " + world);
return world;
}
private String formatBoolean(String format, boolean value) {
return String.format(format, value ? "enabled" : "disabled");
}
// Simple boolean parsing
private boolean parseBoolean(String value) {
if (Arrays.asList("true", "yes", "enabled", "on", "1").contains(value))
return true;
else if (Arrays.asList("false", "no", "disabled", "off", "0").contains(value)) {
return false;
} else {
throw new CommandErrorException("Cannot parse " + value + " as a boolean (yes/no)");
}
}
/**
* Represents a output factory that has a string output,
* @author Kristian
*/
private abstract static class RenameOutputFactory extends RenameProcessorFactory {
protected StringBuilder output = new StringBuilder();
/**
* Retrieve the final output.
* @return The final output.
*/
public String getOutput() {
return output.toString();
}
}
}