/*
* CommandBook
* Copyright (C) 2012 sk89q <http://www.sk89q.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.commandbook;
import com.sk89q.commandbook.commands.PaginatedResult;
import com.sk89q.commandbook.session.PersistentSession;
import com.sk89q.commandbook.session.SessionComponent;
import com.sk89q.commandbook.util.InputUtil;
import com.sk89q.minecraft.util.commands.Command;
import com.sk89q.minecraft.util.commands.CommandContext;
import com.sk89q.minecraft.util.commands.CommandException;
import com.sk89q.minecraft.util.commands.NestedCommand;
import com.zachsthings.libcomponents.ComponentInformation;
import com.zachsthings.libcomponents.Depend;
import com.zachsthings.libcomponents.InjectComponent;
import com.zachsthings.libcomponents.bukkit.BukkitComponent;
import com.zachsthings.libcomponents.config.ConfigurationBase;
import com.zachsthings.libcomponents.config.Setting;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import static com.sk89q.commandbook.util.NestUtil.getNestedMap;
/**
* This component provides command warmups and cooldowns, measured in seconds, by using
* a repeating scheduler task that increases the value for each entry in each CooldownState
* by one each second if the value is less than the number of seconds specified in the
* configuration, removing the entry if the warmup/cooldown has been removed from the configuration
*/
@Depend(components = SessionComponent.class)
@ComponentInformation(friendlyName = "Warmups and Cooldowns", desc = "Allows warmups and cooldowns for commands, specified in seconds.")
public class CooldownsComponent extends BukkitComponent implements Listener, Runnable {
@InjectComponent private SessionComponent sessions;
private LocalConfiguration config;
private static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
@Override
public void enable() {
config = configure(new LocalConfiguration());
CommandBook.registerEvents(this);
registerCommands(Commands.class);
scheduler.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS);
}
@Override
public void reload() {
super.reload();
configure(config);
}
@Override
public void disable() {
scheduler.shutdown();
}
private static String firstWord(final String str) {
int spaceIndex = str.indexOf(" ");
if (spaceIndex == -1) {
return str;
}
return str.substring(0, spaceIndex);
}
private static String titleCase(String input) {
StringBuilder ret = new StringBuilder();
for (String word : input.split(" ")) {
if (ret.length() > 0) {
ret.append(" ");
}
if (ret.length() == 0 || word.length() > 2) {
ret.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
} else {
ret.append(word);
}
}
return ret.toString();
}
private static final SimpleDateFormat timeFormat = new SimpleDateFormat("mm:ss");
private static String formatTime(int timeInSeconds, TimeUnit unit) {
synchronized (timeFormat) {
return timeFormat.format(new Date(unit.toMillis(timeInSeconds)));
}
}
public void run() {
for (final CooldownState state : sessions.getSessions(CooldownState.class).values()) {
final HashSet<String> visitedCooldowns = new HashSet<String>();
for (Iterator<Map.Entry<String, Integer>> i = state.cooldownCommands.entrySet().iterator(); i.hasNext();) {
final Map.Entry<String, Integer> entry = i.next();
final Integer cooldownTime = getNestedMap(config.registeredActions, entry.getKey()).get("cooldown");
if (cooldownTime == null) {
i.remove(); // The cooldown has been removed, so we can get rid of it.
continue;
}
if (entry.getValue() <= cooldownTime) { // Increment the time if it isn't already at the required time
entry.setValue(entry.getValue() + 1);
}
visitedCooldowns.add(entry.getKey());
}
for (Iterator<Map.Entry<String, WarmupInfo>> i = state.warmupCommands.entrySet().iterator(); i.hasNext();) {
final Map.Entry<String, WarmupInfo> entry = i.next();
final Integer warmupTime = getNestedMap(config.registeredActions, entry.getKey()).get("warmup");
if (warmupTime == null) {
i.remove(); // The warmup has been removed, so we can get rid of it.
continue;
} else if (visitedCooldowns.contains(entry.getKey())) {
continue;
}
if (entry.getValue().remainingTime < warmupTime) {
entry.getValue().remainingTime++;
} else if (entry.getValue().remainingTime == warmupTime) {
// Reached the needed time, run a scheduler task to execute the command
// back on the main thread.
final CommandSender owner = state.getOwner();
if (owner != null) {
CommandBook.server().getScheduler().callSyncMethod(CommandBook.inst(), new Callable<Boolean>() {
@Override
public Boolean call() {
return CommandBook.server().dispatchCommand(owner, entry.getValue().fullCommand);
}
});
}
i.remove();
}
}
}
}
private static class LocalConfiguration extends ConfigurationBase {
@Setting("commands"/*, help = "A mapping of commands to their cooldown values." +
"For each command there are both warmup and cooldown values, with numbers " +
"measured in seconds"*/)
public Map<String, Map<String, Integer>> registeredActions = createDefaultStructure();
public Map<String, Map<String, Integer>> createDefaultStructure() {
Map<String, Map<String, Integer>> result = new HashMap<String, Map<String, Integer>>();
getNestedMap(result, "command-with-warmup").put("warmup", 50);
getNestedMap(result, "command-with-cooldown").put("cooldown", 50);
getNestedMap(result, "command-with-warmup-and-cooldown").put("cooldown", 50);
getNestedMap(result, "command-with-warmup-and-cooldown").put("warmup", 50);
return result;
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void playerHandler(PlayerCommandPreprocessEvent event) {
if (!checkCooldown(event.getPlayer(), event.getMessage().substring(1))
|| !checkWarmup(event.getPlayer(), event.getMessage().substring(1))) {
event.setCancelled(true);
}
}
/*@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) // TODO: Make ServerCommandEvent Cancellable
public void serverCommandHandler(ServerCommandEvent event) {
if (!checkCooldown(event.getSender(), event.getCommand())
|| !checkWarmup(event.getSender(), event.getCommand())) {
event.setCancelled(true);
}
}*/
public boolean checkCooldown(CommandSender sender, String command) {
CooldownState state = sessions.getSession(CooldownState.class, sender);
command = firstWord(command);
synchronized (state.cooldownCommands) {
Map<String, Integer> storedTimes = config.registeredActions.get(command);
if (storedTimes == null) { // Nothing in the config for this command
return true;
}
Integer requiredCooldownTime = storedTimes.get("cooldown");
if (requiredCooldownTime == null) { // No cooldown for this command
return true;
}
Integer passedCooldownTime = state.cooldownCommands.get(command);
if (passedCooldownTime == null) { // There isn't an in-progress cooldown for this command
passedCooldownTime = 0;
state.cooldownCommands.put(command, passedCooldownTime);
}
if (passedCooldownTime >= requiredCooldownTime
|| CommandBook.inst().hasPermission(sender, "commandbook.cooldown.override." + command)) {
state.cooldownCommands.remove(command);
return true;
} else {
sender.sendMessage(ChatColor.YELLOW + "The command '" + command +
"' has a remaining cooldown of " +
formatTime(requiredCooldownTime - passedCooldownTime, TimeUnit.SECONDS)
+ " seconds.");
return false;
}
}
}
public boolean checkWarmup(CommandSender sender, String command) {
CooldownState state = sessions.getSession(CooldownState.class, sender);
synchronized (state.warmupCommands) {
Map<String, Integer> storedTimes = config.registeredActions.get(firstWord(command));
if (storedTimes == null) { // Nothing in the config for this command
return true;
}
Integer requiredWarmupTime = storedTimes.get("warmup");
if (requiredWarmupTime == null || CommandBook.inst().hasPermission(sender,
"commandbook.warmup.override." + firstWord(command))) { // No warmup for this command
return true;
}
WarmupInfo warmupInfo = state.warmupCommands.get(firstWord(command));
if (warmupInfo == null) { // There isn't an in-progress warmup for this command
warmupInfo = new WarmupInfo(command);
state.warmupCommands.put(command, warmupInfo);
sender.sendMessage(ChatColor.YELLOW + "Warmup started for command '"
+ firstWord(command) + "', time remaining: " +
formatTime(requiredWarmupTime, TimeUnit.SECONDS) + " seconds");
return false;
}
if (!command.equals(warmupInfo.fullCommand)) {
sender.sendMessage(ChatColor.RED + "You are trying to use the command '"
+ command + "', which already has a warmup in progress. Type /warmup cancel "
+ firstWord(command) + " to cancel the existing warmup" );
}
return false;
}
}
private static class WarmupInfo {
public final String fullCommand;
public int remainingTime;
public WarmupInfo(String fullCommand) {
this.fullCommand = fullCommand;
}
}
private static class CooldownState extends PersistentSession {
public static final long MAX_AGE = TimeUnit.MINUTES.toMillis(30);
public final Map<String, WarmupInfo> warmupCommands = new ConcurrentHashMap<String, WarmupInfo>();
@Setting("cooldown-commands") public final Map<String, Integer> cooldownCommands = new ConcurrentHashMap<String, Integer>();
protected CooldownState() {
super(MAX_AGE);
}
}
public class Commands {
@Command(aliases = {"warmup", "warmups"}, desc = "Provides information about command warmups")
@NestedCommand(WarmupCommands.class)
public void warmup() {}
@Command(aliases = {"cooldown", "cooldowns"}, desc = "Provides information about command cooldowns")
@NestedCommand(CooldownCommands.class)
public void cooldown() {}
}
public class CooldownCommands extends SubCommands<Map.Entry<String, Integer>> {
@Command(aliases = {"list", "ls"}, desc = "List active command limitations", usage = "[-p page] [player]", flags = "p:", min = 0, max = 1)
public void list(CommandContext args, CommandSender sender) throws CommandException {
CommandSender target;
if (args.argsLength() == 0) {
target = sender;
} else {
target = InputUtil.PlayerParser.matchPlayerOrConsole(sender, args.getString(0));
}
getListOutput().display(sender, getActive(target), args.getFlagInteger('p', 1));
}
@Command(aliases = {"cancel", "c"}, desc = "Cancel a command limitation", usage = "<cmd>", min = 1)
public void cancel(CommandContext args, CommandSender sender) throws CommandException {
String item = args.getJoinedStrings(0);
if (remove(sender, item)) {
sender.sendMessage(ChatColor.YELLOW + titleCase(getTypeName()) +
" for command '" + item + "' removed.");
} else {
throw new CommandException("No " + getTypeName() + " for input " + item);
}
}
@Override
public String getTypeName() {
return "cooldown";
}
@Override
public PaginatedResult<Map.Entry<String, Integer>> getListOutput() {
return new PaginatedResult<Map.Entry<String, Integer>>("Command - Time remaining") {
@Override
public String format(Map.Entry<String, Integer> entry) {
Map<String, Integer> storedTimes = config.registeredActions.get(entry.getKey());
if (storedTimes == null) { // Nothing in the config for this command
return "Invalid cooldown: " + entry.getKey();
}
Integer requiredCooldownTime = storedTimes.get("cooldown");
if (requiredCooldownTime == null) { // No cooldown for this command
return "Invalid cooldown: " + entry.getKey();
}
return entry.getKey() + " - " +
formatTime(requiredCooldownTime - entry.getValue(), TimeUnit.SECONDS);
}
};
}
@Override
public Collection<Map.Entry<String, Integer>> getActive(CommandSender sender) {
return sessions.getSession(CooldownState.class, sender).cooldownCommands.entrySet();
}
@Override
public boolean remove(CommandSender sender, String name) {
return sessions.getSession(CooldownState.class, sender).cooldownCommands.remove(name.toLowerCase()) != null;
}
}
public class WarmupCommands extends SubCommands<WarmupInfo> {
@Command(aliases = {"list", "ls"}, desc = "List active command limitations", usage = "[-p page] [player]", flags = "p:", min = 0, max = 1)
public void list(CommandContext args, CommandSender sender) throws CommandException {
CommandSender target;
if (args.argsLength() == 0) {
target = sender;
} else {
target = InputUtil.PlayerParser.matchPlayerOrConsole(sender, args.getString(0));
}
getListOutput().display(sender, getActive(target), args.getFlagInteger('p', 1));
}
@Command(aliases = {"cancel", "c"}, desc = "Cancel a command limitation", usage = "<cmd>", min = 1)
public void cancel(CommandContext args, CommandSender sender) throws CommandException {
String item = args.getJoinedStrings(0);
if (remove(sender, item)) {
sender.sendMessage(ChatColor.YELLOW + titleCase(getTypeName()) +
" for command '" + item + "' removed.");
} else {
throw new CommandException("No " + getTypeName() + " for input " + item);
}
}
@Override
public String getTypeName() {
return "warmup";
}
@Override
public PaginatedResult<WarmupInfo> getListOutput() {
return new PaginatedResult<WarmupInfo>("Command - Remaining time") {
@Override
public String format(WarmupInfo entry) {
Map<String, Integer> storedTimes = config.registeredActions
.get(firstWord(entry.fullCommand));
if (storedTimes == null) { // Nothing in the config for this command
return "Invalid warmup: " + entry.fullCommand;
}
Integer requiredWarmupTime = storedTimes.get("warmup");
if (requiredWarmupTime == null) { // No cooldown for this command
return "Invalid warmup: " + entry.fullCommand;
}
return "/" + entry.fullCommand + " - " +
formatTime(requiredWarmupTime - entry.remainingTime, TimeUnit.SECONDS);
}
};
}
@Override
public Collection<WarmupInfo> getActive(CommandSender sender) {
return sessions.getSession(CooldownState.class, sender).warmupCommands.values();
}
@Override
public boolean remove(CommandSender sender, String name) {
return sessions.getSession(CooldownState.class, sender).warmupCommands.remove(name.toLowerCase()) != null;
}
}
private abstract class SubCommands<CommandType> {
public abstract String getTypeName();
public abstract PaginatedResult<CommandType> getListOutput();
public abstract Collection<CommandType> getActive(CommandSender sender);
public abstract boolean remove(CommandSender sender, String name);
/*@Command(aliases = {"list", "ls"}, desc = "List active command limitations", usage = "[-p page] [player]", flags = "p:", min = 0, max = 1)
public void list(CommandContext args, CommandSender sender) throws CommandException {
CommandSender target;
if (args.argsLength() == 0) {
target = sender;
} else {
target = InputUtil.PlayerParser.matchPlayerOrConsole(sender, args.getString(0));
}
getListOutput().display(sender, getActive(target), args.getFlagInteger('p', 1));
}
@Command(aliases = {"cancel", "c"}, desc = "Cancel a command limitation", usage = "<cmd>", min = 1)
public void cancel(CommandContext args, CommandSender sender) throws CommandException {
String item = args.getJoinedStrings(0);
if (remove(sender, item)) {
sender.sendMessage(ChatColor.YELLOW + titleCase(getTypeName()) +
" for command '" + item + "' removed.");
} else {
throw new CommandException("No " + getTypeName() + " for input " + item);
}
}*/
}
}