Add the "ncp top" command, allowing to search all the violation history.

Original pull request:
https://github.com/NoCheatPlus/NoCheatPlus/pull/24

This probably is not the final implementation, but it allows some
minimal freedom:
* Specify number of entries to show.
* Specify check types (and groups!).
* Specify what to sort by.

There might be need for some merged view, combining several different
check types somehow, or just shortcuts for specific selections, e.g. for
fighting-related checks.

----

+ Fix root command not showing sub commmand usage.
This commit is contained in:
asofold 2014-07-29 12:40:54 +02:00
parent 9b6c717fc0
commit c2722abc19
6 changed files with 557 additions and 161 deletions

View File

@ -0,0 +1,37 @@
package fr.neatmonster.nocheatplus.utilities;
import java.util.Collection;
import java.util.Comparator;
/**
* Allow to sort by multiple criteria, first come first serve.
* @author dev1mc
*
*/
public class FCFSComparator <T> implements Comparator<T> {
private final Comparator<T>[] comparators;
private final boolean reverse;
public FCFSComparator(Collection<Comparator<T>> comparators) {
this(comparators, false);
}
@SuppressWarnings("unchecked")
public FCFSComparator(Collection<Comparator<T>> comparators, boolean reverse) {
this.comparators = (Comparator<T>[]) comparators.toArray();
this.reverse = reverse;
}
@Override
public int compare(T o1, T o2) {
for (int i = 0; i < comparators.length; i++) {
final int res = comparators[i].compare(o1, o2);
if (res != 0) {
return reverse ? -res : res;
}
}
return 0;
}
}

View File

@ -5,12 +5,17 @@ import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.bukkit.entity.Player;
import fr.neatmonster.nocheatplus.hooks.APIUtils;
import fr.neatmonster.nocheatplus.utilities.FCFSComparator;
/**
* The class containg the violation history of a player.
@ -22,15 +27,14 @@ public class ViolationHistory {
* (Comparable by time.)
*/
public static class ViolationLevel{
/**
* Descending sort.
* Descending sort by time.
*/
public static Comparator<ViolationLevel> VLComparator = new Comparator<ViolationHistory.ViolationLevel>() {
@Override
public int compare(final ViolationLevel vl1, final ViolationLevel vl2) {
if (vl1.time == vl2.time) return 0;
else if (vl1.time < vl2.time) return 1;
else return -1;
return Long.compare(vl1.time, vl2.time);
}
};
@ -93,6 +97,114 @@ public class ViolationHistory {
}
}
public static class VLView {
public static final Comparator<VLView> CmpName = new Comparator<ViolationHistory.VLView>() {
@Override
public int compare(VLView o1, VLView o2) {
return o1.name.compareToIgnoreCase(o2.name);
}
};
public static final Comparator<VLView> CmpCheck = new Comparator<ViolationHistory.VLView>() {
@Override
public int compare(VLView o1, VLView o2) {
return o1.check.compareToIgnoreCase(o2.check);
}
};
public static final Comparator<VLView> CmpSumVL = new Comparator<ViolationHistory.VLView>() {
@Override
public int compare(VLView o1, VLView o2) {
return Double.compare(o1.sumVL, o2.sumVL);
}
};
public static final Comparator<VLView> CmpnVL = new Comparator<ViolationHistory.VLView>() {
@Override
public int compare(VLView o1, VLView o2) {
return Integer.compare(o1.nVL, o2.nVL);
}
};
public static final Comparator<VLView> CmpAvgVL = new Comparator<ViolationHistory.VLView>() {
@Override
public int compare(VLView o1, VLView o2) {
return Double.compare(o1.sumVL / o1.nVL, o2.sumVL / o2.nVL);
}
};
public static final Comparator<VLView> CmpMaxVL = new Comparator<ViolationHistory.VLView>() {
@Override
public int compare(VLView o1, VLView o2) {
return Double.compare(o1.maxVL, o2.maxVL);
}
};
public static final Comparator<VLView> CmpTime = new Comparator<ViolationHistory.VLView>() {
@Override
public int compare(VLView o1, VLView o2) {
return Long.compare(o1.time, o2.time);
}
};
/**
* Get a mixed/fcfs comparator from parsing given args. Accepted are
* @param args
* @param startIndex
* @return If none are found, null is returned, no errors will be thrown, duplicates are removed.
*/
public static Comparator<VLView> parseMixedComparator(String[] args, int startIndex) {
final Set<Comparator<VLView>> comparators = new LinkedHashSet<Comparator<VLView>>();
for (int i = startIndex; i < args.length; i ++) {
String arg = args[i].toLowerCase();
while (arg.startsWith("-")) {
arg = arg.substring(1);
}
if (arg.matches("(name|player|playername)")) {
comparators.add(CmpName);
} else if (arg.matches("(check|type|checktype)")) {
comparators.add(CmpCheck);
} else if (arg.matches("(sum|sumvl|vl)")) {
comparators.add(CmpSumVL);
} else if (arg.matches("(n|num|number|nvl)")) {
comparators.add(CmpnVL);
} else if (arg.matches("(avg|av|average|averagevl|avgvl|avvl|avl)")) {
comparators.add(CmpAvgVL);
} else if (arg.matches("(max|maxvl|maximum|maximumvl)")) {
comparators.add(CmpMaxVL);
} else if (arg.matches("(time|t)")) {
comparators.add(CmpTime);
}
}
if (comparators.isEmpty()) {
return null;
}
return new FCFSComparator<ViolationHistory.VLView>(comparators, true);
}
public final String name;
public final String check;
public final double sumVL;
public final int nVL;
public final double maxVL;
public final long time;
public VLView(String name, ViolationLevel vl) {
this(name, vl.check, vl.sumVL, vl.nVL, vl.maxVL, vl.time);
}
public VLView(String name, String check, double sumVL, int nVL, double maxVL, long time) {
this.name = name;
this.check = check;
this.sumVL = sumVL;
this.nVL = nVL;
this.maxVL = maxVL;
this.time = time;
}
}
/** Map the check string names to check types (workaround, keep at default, set by Check)*/
static Map<String, CheckType> checkTypeMap = new HashMap<String, CheckType>();
@ -144,6 +256,23 @@ public class ViolationHistory {
return null;
}
/**
* Get a list of VLView instances for direct check type matches (no inheritance checks).
* @param checkType
* @return Always returns a list.
*/
public static List<VLView> getView(final CheckType checkType) {
final List<VLView> view = new LinkedList<VLView>();
for (final Entry<String, ViolationHistory> entry: violationHistories.entrySet()) {
final ViolationHistory hist = entry.getValue();
final ViolationLevel vl = hist.getViolationLevel(checkType);
if (vl != null) {
view.add(new VLView(entry.getKey(), vl));
}
}
return view;
}
public static ViolationHistory removeHistory(final String playerName){
return violationHistories.remove(playerName);
}
@ -158,7 +287,7 @@ public class ViolationHistory {
private final List<ViolationLevel> violationLevels = new ArrayList<ViolationLevel>();
/**
* Gets the violation levels.
* Gets the violation levels. Sorted by time, descending.
*
* @return the violation levels
*/
@ -169,6 +298,21 @@ public class ViolationHistory {
return sortedLevels;
}
/**
* Return only direct matches, no inheritance checking.
* @param type
* @return ViolationLevel instance, if present. Otherwise null.
*/
public ViolationLevel getViolationLevel(final CheckType type) {
for (int i = 0; i < violationLevels.size(); i++) {
final ViolationLevel vl = violationLevels.get(i);
if (checkTypeMap.get(vl.check) == type) {
return vl;
}
}
return null;
}
/**
* Log a VL.
*

View File

@ -31,6 +31,7 @@ import fr.neatmonster.nocheatplus.command.admin.exemption.UnexemptCommand;
import fr.neatmonster.nocheatplus.command.admin.log.LogCommand;
import fr.neatmonster.nocheatplus.command.admin.notify.NotifyCommand;
import fr.neatmonster.nocheatplus.command.admin.reset.ResetCommand;
import fr.neatmonster.nocheatplus.command.admin.top.TopCommand;
import fr.neatmonster.nocheatplus.components.INotifyReload;
import fr.neatmonster.nocheatplus.config.ConfPaths;
import fr.neatmonster.nocheatplus.config.ConfigFile;
@ -86,6 +87,7 @@ public class NoCheatPlusCommand extends BaseCommand{
new DelayCommand(plugin),
new ExemptCommand(plugin),
new ExemptionsCommand(plugin),
new TopCommand(plugin),
new InfoCommand(plugin),
new InspectCommand(plugin),
new KickCommand(plugin),
@ -138,7 +140,13 @@ public class NoCheatPlusCommand extends BaseCommand{
AbstractCommand<?> subCommand = subCommands.get(args[0].trim().toLowerCase());
if (subCommand != null && subCommand.testPermission(sender, command, commandLabel, args)){
// Sender has permission to run the command.
return subCommand.onCommand(sender, command, commandLabel, args);
final boolean res = subCommand.onCommand(sender, command, commandLabel, args);
if (!res && subCommand.usage != null) {
sender.sendMessage(subCommand.usage);
return true;
} else {
return res;
}
}
}
// No sub command worked, print usage.
@ -157,51 +165,51 @@ public class NoCheatPlusCommand extends BaseCommand{
}
}
// /**
// * Check which of the choices starts with prefix
// * @param sender
// * @param choices
// * @return
// */
// protected List<String> getTabMatches(CommandSender sender, Collection<String> choices, String prefix){
// final List<String> res = new ArrayList<String>(choices.size());
// final Set<BaseCommand> done = new HashSet<BaseCommand>();
// for (final String label : choices){
// if (!label.startsWith(prefix)) continue;
// final BaseCommand cmd = commands.get(label);
// if (done.contains(cmd)) continue;
// done.add(cmd);
// if (sender.hasPermission(cmd.permission)) res.add(cmd.label);
// }
// if (!res.isEmpty()){
// Collections.sort(res);
// return res;
// }
// return null;
// }
// /**
// * Check which of the choices starts with prefix
// * @param sender
// * @param choices
// * @return
// */
// protected List<String> getTabMatches(CommandSender sender, Collection<String> choices, String prefix){
// final List<String> res = new ArrayList<String>(choices.size());
// final Set<BaseCommand> done = new HashSet<BaseCommand>();
// for (final String label : choices){
// if (!label.startsWith(prefix)) continue;
// final BaseCommand cmd = commands.get(label);
// if (done.contains(cmd)) continue;
// done.add(cmd);
// if (sender.hasPermission(cmd.permission)) res.add(cmd.label);
// }
// if (!res.isEmpty()){
// Collections.sort(res);
// return res;
// }
// return null;
// }
// @Override
// public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args)
// {
// // TODO: TabComplete check ?
// if (args.length == 0 || args.length == 1 && args[0].trim().isEmpty()){
// // Add labels without aliases.
// return getTabMatches(sender, rootLabels, "");
// }
// else {
// final String subLabel = args[0].trim().toLowerCase();
// if (args.length == 1){
// // Also check aliases for matches.
// return getTabMatches(sender, commands.keySet(), subLabel);
// }
// else{
// final NCPCommand cmd = commands.get(subLabel);
// if (cmd.testPermission...){
// // Delegate the tab-completion.
// return cmd.onTabComplete(sender, command, alias, args);
// }
// }
// }
// return null;
// }
// @Override
// public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args)
// {
// // TODO: TabComplete check ?
// if (args.length == 0 || args.length == 1 && args[0].trim().isEmpty()){
// // Add labels without aliases.
// return getTabMatches(sender, rootLabels, "");
// }
// else {
// final String subLabel = args[0].trim().toLowerCase();
// if (args.length == 1){
// // Also check aliases for matches.
// return getTabMatches(sender, commands.keySet(), subLabel);
// }
// else{
// final NCPCommand cmd = commands.get(subLabel);
// if (cmd.testPermission...){
// // Delegate the tab-completion.
// return cmd.onTabComplete(sender, command, alias, args);
// }
// }
// }
// return null;
// }
}

View File

@ -0,0 +1,202 @@
package fr.neatmonster.nocheatplus.command.admin.top;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.ViolationHistory;
import fr.neatmonster.nocheatplus.checks.ViolationHistory.VLView;
import fr.neatmonster.nocheatplus.command.BaseCommand;
import fr.neatmonster.nocheatplus.hooks.APIUtils;
import fr.neatmonster.nocheatplus.permissions.Permissions;
import fr.neatmonster.nocheatplus.utilities.FCFSComparator;
public class TopCommand extends BaseCommand{
protected static class PrimaryThreadWorker implements Runnable{
private final Collection<CheckType> checkTypes;
private final CommandSender sender;
private final Comparator<VLView> comparator;
private final int n;
private final Plugin plugin;
public PrimaryThreadWorker(CommandSender sender, Collection<CheckType> checkTypes, Comparator<VLView> comparator, int n, Plugin plugin) {
this.checkTypes = new LinkedHashSet<CheckType>(checkTypes);
this.sender = sender;
this.comparator = comparator;
this.n = n;
this.plugin = plugin;
}
@Override
public void run() {
final Iterator<CheckType> it = checkTypes.iterator();
List<VLView> views = null;
CheckType type = null;
while (it.hasNext()) {
type = it.next();
it.remove();
views = ViolationHistory.getView(type);
if (views.isEmpty()) {
views = null;
} else {
break;
}
}
if (views == null) {
sender.sendMessage("No more history to process.");
} else {
// Start sorting and result processing asynchronously.
Bukkit.getScheduler().runTaskAsynchronously(plugin,
new AsynchronousWorker(sender, type, views, checkTypes, comparator, n, plugin));
}
}
}
protected static class AsynchronousWorker implements Runnable{
private final CommandSender sender;
private final CheckType checkType;
private final List<VLView> views;
private final Collection<CheckType> checkTypes;
private final Comparator<VLView> comparator;
private final int n;
private final Plugin plugin;
public AsynchronousWorker(CommandSender sender, CheckType checkType, List<VLView> views, Collection<CheckType> checkTypes, Comparator<VLView> comparator, int n, Plugin plugin) {
this.sender = sender;
this.checkType = checkType;
this.views = views;
this.checkTypes = checkTypes;
this.comparator = comparator;
this.n = n;
this.plugin = plugin;
}
@Override
public void run() {
final DecimalFormat format = new DecimalFormat("#.#");
// Sort
Collections.sort(views, comparator);
// Display.
final StringBuilder builder = new StringBuilder(100 + 32 * views.size());
builder.append(checkType.toString());
builder.append(":");
final String c1, c2;
if (sender instanceof Player) {
c1 = ChatColor.WHITE.toString();
c2 = ChatColor.GRAY.toString();
} else {
c1 = c2 = "";
}
int done = 0;
for (final VLView view : views) {
builder.append(" " + c1);
builder.append(view.name);
// Details
builder.append(c2 + "(");
// sum
builder.append("sum=");
builder.append(format.format(view.sumVL));
// n
builder.append("/n=");
builder.append(view.nVL);
// avg
builder.append("/avg=");
builder.append(format.format(view.sumVL / view.nVL));
// max
builder.append("/max=");
builder.append(format.format(view.maxVL));
builder.append(")");
if (done >= n) {
break;
}
}
if (views.isEmpty()) {
builder.append(c1 + "Nothing to display.");
}
final String message = builder.toString();
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin,
new Runnable() {
@Override
public void run() {
sender.sendMessage(message);
}
});
if (!checkTypes.isEmpty()) {
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin,
new PrimaryThreadWorker(sender, checkTypes, comparator, n, plugin));
}
}
}
public TopCommand(JavaPlugin plugin) {
super(plugin, "top", Permissions.COMMAND_TOP);
this.usage = "Optional: Specify number of entries to show (once).\nObligatory: Specify check types (multiple possible).\nOptional: Specify what to sort by (multiple possible: -sumvl, -avgvl, -maxvl, -nvl, -name, -time).\nThis is a heavy operation, use with care."; // -check
}
@Override
public boolean onCommand(CommandSender sender, Command command, String alias, String[] args) {
if (args.length < 2) {
return false;
}
int startIndex = 1;
Integer n = 10;
try {
n = Integer.parseInt(args[1].trim());
startIndex = 2;
} catch (NumberFormatException e) {}
if (n <= 0) {
sender.sendMessage("Setting number of entries to 10");
n = 1;
} else if ((sender instanceof Player) && n > 300) {
sender.sendMessage("Capping number of entries at 300.");
n = 300;
} else if (n > 10000) {
sender.sendMessage("Capping number of entries at 10000.");
n = 10000;
}
Set<CheckType> checkTypes = new LinkedHashSet<CheckType>();
for (int i = startIndex; i < args.length; i ++) {
CheckType type = null;
try {
type = CheckType.valueOf(args[i].trim().toUpperCase().replace('-', '_').replace('.', '_'));
} catch (Throwable t) {} // ...
if (type != null) {
checkTypes.addAll(APIUtils.getChildren(type)); // Includes type.
}
}
if (checkTypes.isEmpty()) {
sender.sendMessage("No check types specified!");
return false;
}
Comparator<VLView> comparator = VLView.parseMixedComparator(args, startIndex);
if (comparator == null) {
// TODO: Default comparator ?
comparator = new FCFSComparator<ViolationHistory.VLView>(Arrays.asList(VLView.CmpnVL), true);
}
// Run a worker task.
Bukkit.getScheduler().scheduleSyncDelayedTask(access,
new PrimaryThreadWorker(sender, checkTypes, comparator, n, access));
return true;
}
// TODO: Tab completion (!).
}

View File

@ -35,6 +35,7 @@ public class Permissions {
public static final String COMMAND_RELOAD = COMMAND + ".reload";
public static final String COMMAND_REMOVEPLAYER = COMMAND + ".removeplayer";
public static final String COMMAND_RESET = COMMAND + ".reset";
public static final String COMMAND_TOP = COMMAND + ".top";
public static final String COMMAND_UNEXEMPT = COMMAND + ".unexempt";
public static final String COMMAND_VERSION = COMMAND + ".version";

View File

@ -19,6 +19,7 @@ commands:
# permissions: nocheatplus.admin.(...)
usage: |
Administrative commands overview:
/<command> top (entries) (check/s...) (sort by...) NEW.
/<command> info (player): Violation summary for a player.
/<command> inspect (player): Status info for a player.
/<command> notify on|off: In-game notifications per player.
@ -267,6 +268,8 @@ permissions:
nocheatplus.notify: true
nocheatplus.command.reload:
description: Allow the player to reload NoCheatPlus configuration.
nocheatplus.command.top:
description: Allow to search violation history for top violations.
nocheatplus.command.info:
description: Allow to see violation info about a player.
nocheatplus.command.inspect:
@ -328,6 +331,7 @@ permissions:
description: Info commands about players.
children:
nocheatplus.command.notify: true
nocheatplus.command.top: true
nocheatplus.command.info: true
nocheatplus.command.exemptions: true
nocheatplus.command.kicklist: true