Merge pull request #3571 from JLyne/tabcomplete

Add tab completions for commands
This commit is contained in:
mikeprimm 2021-12-28 10:16:05 -06:00 committed by GitHub
commit 48ba0b2e41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 748 additions and 5 deletions

View File

@ -27,9 +27,12 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@ -1117,6 +1120,11 @@ public class DynmapCore implements DynmapCommonAPI {
/* Parse argument strings : handle quoted strings */
public static String[] parseArgs(String[] args, DynmapCommandSender snd) {
return parseArgs(args, snd, false);
}
/* Parse argument strings : handle quoted strings */
public static String[] parseArgs(String[] args, DynmapCommandSender snd, boolean allowUnclosedQuotes) {
ArrayList<String> rslt = new ArrayList<String>();
/* Build command line, so we can parse our way - make sure there is trailing space */
String cmdline = "";
@ -1146,9 +1154,15 @@ public class DynmapCore implements DynmapCommonAPI {
sb.append(c);
}
}
if(inquote) { /* If still in quote, syntax error */
snd.sendMessage("Error: unclosed doublequote");
return null;
if(inquote) { // If still in quote
if(allowUnclosedQuotes) {
if(sb.length() > 1) { // Add remaining input without trailing space
rslt.add(sb.substring(0, sb.length() - 1));
}
} else { // Syntax error
snd.sendMessage("Error: unclosed doublequote");
return null;
}
}
return rslt.toArray(new String[rslt.size()]);
}
@ -1251,7 +1265,7 @@ public class DynmapCore implements DynmapCommonAPI {
new CommandInfo("dmarker", "movehere", "id:<id>", "Move marker with ID <id> to current location."),
new CommandInfo("dmarker", "update", "<label> icon:<icon> newlabel:<newlabel>", "Update marker with ID <id> with new label <newlabel> and new icon <icon>."),
new CommandInfo("dmarker", "delete", "<label>", "Delete marker with label of <label>."),
new CommandInfo("dmarker", "delete ", "id:<id>", "Delete marker with ID of <id>."),
new CommandInfo("dmarker", "delete", "id:<id>", "Delete marker with ID of <id>."),
new CommandInfo("dmarker", "list", "List details of all markers."),
new CommandInfo("dmarker", "icons", "List details of all icons."),
new CommandInfo("dmarker", "addset", "<label>", "Add marker set with label <label>."),
@ -1351,7 +1365,164 @@ public class DynmapCore implements DynmapCommonAPI {
}
sender.sendMessage(subcmdlist);
}
/**
* Returns tab completion suggestions for subcommands
*
* @param sender - The command sender requesting the tab completion suggestions
* @param cmd - The top level command to suggest for
* @param arg - Optional partial subcommand name to filter by
* @return List of tab completion suggestions
*/
List<String> getSubcommandSuggestions(DynmapCommandSender sender, String cmd, String arg) {
List<String> suggestions = new ArrayList<>();
for (CommandInfo ci : commandinfo) {
//TODO: Permission checks
if (ci.matches(cmd) && ci.subcmd.startsWith(arg) && !suggestions.contains(ci.subcmd)) {
suggestions.add(ci.subcmd);
}
}
return suggestions;
}
/**
* Returns tab completion suggestions for world names
*
* @param arg - Partial world name to filter by
* @return List of tab completion suggestions
*/
public List<String> getWorldSuggestions(String arg) {
return mapManager.getWorlds().stream()
.map(DynmapWorld::getName)
.filter(name -> name.startsWith(arg))
.collect(Collectors.toList());
}
/**
* Returns tab completion suggestions for map names of a specific world
*
* @param worldName - Name of the world
* @param mapArg - Partial map name to filter by
* @param colonSeparated - Whether to return suggestions in world:map format
* @return List of tab completion suggestions
*/
List<String> getMapSuggestions(String worldName, String mapArg, boolean colonSeparated) {
DynmapWorld world = mapManager.getWorld(worldName);
if (world != null) {
//Don't suggest anything if the argument contains a space as the client doesn't handle this well
if(mapArg.contains(" ")) {
return Collections.emptyList();
}
return world.maps.stream()
.filter(map -> map.getName().startsWith(mapArg))
.map(map -> {
if (map.getName().contains(" ")) { //Quote if map name contains a space
return "\"" + (colonSeparated ? worldName + ":" + map.getName() : map.getName()) + "\"";
} else {
return colonSeparated ? worldName + ":" + map.getName() : map.getName();
}
})
.collect(Collectors.toList());
}
return Collections.emptyList();
}
/**
* Returns tab completion suggestions for map names without a world name, in world:map format
*
* @param arg - Partial world:map name to filter by
* @return List of tab completion suggestions
*/
List<String> getMapSuggestions(String arg) {
int colon = arg.indexOf(":");
final String worldName = (colon >= 0) ? arg.substring(0, colon) : arg;
String mapArg = (colon >= 0) ? arg.substring(colon + 1) : null;
//Don't suggest anything if the argument contains a space as the client doesn't handle this well
if(arg.contains(" ")) {
return Collections.emptyList();
}
if (mapArg != null) {
return getMapSuggestions(worldName, mapArg, true);
}
List<String> suggestions = new ArrayList<>();
mapManager.getWorlds().stream()
.filter(world -> world.getName().startsWith(worldName))
.forEach(world -> {
List<String> maps = world.maps.stream()
.map(map -> {
if (map.getName().contains(" ")) { //Quote if map name contains a space
return "\"" + world.getName() + ":" + map.getName() + "\"";
} else {
return world.getName() + ":" + map.getName();
}
})
.collect(Collectors.toList());
suggestions.addAll(maps);
});
return suggestions;
}
/**
* Returns tab completion suggestions for field:value args based on the provided arguments
* If the last provided argument contains a ":", values for the field will be suggested if present
* Otherwise fields will be suggested if they do not already exist with a value in the provided arguments
*
* @param args - Array of already provided command arguments
* @param fields - Map of possible field names and suppliers for values
* @return List of tab completion suggestions
*/
public List<String> getFieldValueSuggestions(String[] args, Map<String, Supplier<String[]>> fields) {
if (args.length == 0 || fields == null || fields.isEmpty()) {
return Collections.emptyList();
}
List<String> suggestions = new ArrayList<>(fields.keySet());
String[] lastArgument = args[args.length - 1].split(":", 2);
//If last argument is an incomplete field value, suggest matching values for that field.
if (lastArgument.length == 2) {
if (fields.containsKey(lastArgument[0])) {
//Don't suggest anything if the value contains a space as the client doesn't handle this well
if(lastArgument[1].contains(" ")) {
return Collections.emptyList();
}
return Arrays.stream(fields.get(lastArgument[0]).get())
.filter(value -> value.startsWith(lastArgument[1]))
//Format suggestions as field:value, quoting the value if it contains a space
.map(value -> lastArgument[0] + ":" + (value.contains(" ") ? "\"" + value + "\"" : value))
.collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
//Remove fields with values in previous args from suggestions
for (String arg : args) {
String[] value = arg.split(":");
if (suggestions.contains(value[0]) && value.length == 2) {
suggestions.remove(value[0]);
}
}
//Suggest remaining fields
return suggestions.stream().
filter(field -> field.startsWith(args[args.length - 1]))
.map(field -> field + ":")
.collect(Collectors.toList());
}
public boolean processCommand(DynmapCommandSender sender, String cmd, String commandLabel, String[] args) {
if (mapManager == null) { // Initialization faulure
sender.sendMessage("Dynmap failed to initialize properly: commands not available");
@ -1729,6 +1900,155 @@ public class DynmapCore implements DynmapCommonAPI {
return true;
}
/**
* Returns a list of tab completion suggestions for the given sender, command and command arguments.
*
* @param sender - The sender of the tab completion, used for permission checks
* @param cmd - The top level command being tab completed
* @param args - Array of extra command arguments
* @return List of tab completion suggestions
*/
public List<String> getTabCompletions(DynmapCommandSender sender, String cmd, String[] args) {
if (mapManager == null || args.length == 0) {
return Collections.emptyList();
}
if (args.length == 1) {
return getSubcommandSuggestions(sender, cmd, args[0]);
}
if (cmd.equalsIgnoreCase("dmap")) {
return dmapcmds.getTabCompletions(sender, args, this);
}
if (cmd.equalsIgnoreCase("dmarker")) {
return markerapi.getTabCompletions(sender, args, this);
}
if (cmd.equalsIgnoreCase("dynmapexp")) {
return dynmapexpcmds.getTabCompletions(sender, args, this);
}
if (!cmd.equalsIgnoreCase("dynmap")) {
return Collections.emptyList();
}
/* Re-parse args - handle double quotes */
args = parseArgs(args, sender, true);
if (args == null || args.length <= 1) {
return Collections.emptyList();
}
String subcommand = args[0];
DynmapPlayer player = null;
if (sender instanceof DynmapPlayer) {
player = (DynmapPlayer) sender;
}
if (subcommand.equals("radiusrender") && checkPlayerPermission(sender, "radiusrender")) {
if(args.length == 2) { // /dynmap radiusrender *<world>* <x> <z> <radius> <map>
return getWorldSuggestions(args[1]);
} if(args.length == 3 && player != null) { // /dynmap radiusrender <radius> *<mapname>*
Scanner sc = new Scanner(args[1]);
if(sc.hasNextInt(10)) { //Only show map suggestions if a number was entered before
return getMapSuggestions(player.getLocation().world, args[2], false);
}
} else if(args.length == 6) { // /dynmap radiusrender <world> <x> <z> <radius> *<map>*
return getMapSuggestions(args[1], args[5], false);
}
} else if (subcommand.equals("updaterender") && checkPlayerPermission(sender, "updaterender")) {
if(args.length == 2) { // /dynmap updaterender *<world>* <x> <z> <map>/*<map>*
List<String> suggestions = getWorldSuggestions(args[1]);
if(player != null) {
suggestions.addAll(getMapSuggestions(player.getLocation().world, args[1], false));
}
return suggestions;
} else if(args.length == 5) { // /dynmap updaterender <world> <x> <z> *<map>*
return getMapSuggestions(args[1], args[4], false);
}
} else if (subcommand.equals("hide") && checkPlayerPermission(sender, "hide.others")) {
if(args.length == 2) { // /dynmap hide *<player>*
final String arg = args[1];
return playerList.getVisiblePlayers().stream()
.map(DynmapPlayer::getName)
.filter(name -> name.startsWith(arg))
.collect(Collectors.toList());
}
} else if (subcommand.equals("show") && checkPlayerPermission(sender, "show.others")) {
if(args.length == 2) { // /dynmap show *<player>*
final String arg = args[1];
return playerList.getHiddenPlayers().stream()
.map(DynmapPlayer::getName)
.filter(name -> name.startsWith(arg))
.collect(Collectors.toList());
}
} else if (subcommand.equals("fullrender") && checkPlayerPermission(sender, "fullrender")) {
List<String> suggestions = getWorldSuggestions(args[args.length - 1]); //World suggestions
suggestions.addAll(getMapSuggestions(args[args.length - 1])); //world:map suggestions
//Remove suggestions present in other arguments
for (String arg : args) {
suggestions.remove(arg.contains(" ") ? "\"" + arg + "\"" : arg);
}
//Add resume if previous argument wasn't resume
if ("resume".startsWith(args[args.length - 1])
&& (args.length == 2 || !args[args.length - 2].equals("resume"))) {
suggestions.add("resume");
}
return suggestions;
} else if ((subcommand.equals("cancelrender") && checkPlayerPermission(sender, "cancelrender"))
|| (subcommand.equals("purgequeue") && checkPlayerPermission(sender, "purgequeue"))) {
List<String> suggestions = getWorldSuggestions(args[args.length - 1]);
suggestions.removeAll(Arrays.asList(args)); //Remove worlds present in other arguments
return suggestions;
} else if (subcommand.equals("purgemap") && checkPlayerPermission(sender, "purgemap")) {
if (args.length == 2) { // /dynmap purgemap *<world>* <map>
return getWorldSuggestions(args[1]);
} else if (args.length == 3) { // /dynmap purgemap <world> *<map>*
return getMapSuggestions(args[1], args[2], false);
}
} else if ((subcommand.equals("purgeworld") && checkPlayerPermission(sender, "purgeworld"))
|| (subcommand.equals("stats") && checkPlayerPermission(sender, "stats"))
|| (subcommand.equals("resetstats") && checkPlayerPermission(sender, "resetstats"))) {
if (args.length == 2) {
return getWorldSuggestions(args[1]);
}
} else if (subcommand.equals("pause") && checkPlayerPermission(sender, "pause")) {
List<String> suggestions = Arrays.asList("full", "update", "all", "none");
if (args.length == 2) {
final String arg = args[1];
return suggestions.stream().filter(suggestion -> suggestion.startsWith(arg))
.collect(Collectors.toList());
}
} else if((subcommand.equals("ips-for-id") && checkPlayerPermission(sender, "ips-for-id"))
|| (subcommand.equals("add-id-for-ip") && checkPlayerPermission(sender, "add-id-for-ip"))
|| (subcommand.equals("del-id-for-ip") && checkPlayerPermission(sender, "del-id-for-ip"))
|| (subcommand.equals("webregister") && checkPlayerPermission(sender, "webregister.other"))) {
if(args.length == 2) {
final String arg = args[1];
return Arrays.stream(playerList.getOnlinePlayers())
.map(DynmapPlayer::getName)
.filter(name -> name.startsWith(arg))
.collect(Collectors.toList());
}
} else if(subcommand.equals("help")) {
if(args.length == 2) {
return getSubcommandSuggestions(sender, "dynmap", args[1]);
}
}
return Collections.emptyList();
}
public boolean checkPlayerPermission(DynmapCommandSender sender, String permission) {
if (!(sender instanceof DynmapPlayer) || sender.isOp()) {
return true;

View File

@ -1,11 +1,16 @@
package org.dynmap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Supplier;
import org.dynmap.common.DynmapCommandSender;
import org.dynmap.common.DynmapPlayer;
@ -22,6 +27,76 @@ import org.dynmap.utils.VisibilityLimit;
* Handler for world and map edit commands (via /dmap)
*/
public class DynmapMapCommands {
private Map<String, Map<String, Supplier<String[]>>> tabCompletions = null;
/**
* Generates a map of field:value argument tab completion suggestions for every /dmap subcommand
*/
private void initTabCompletions() {
//Static values
String[] emptyValue = new String[]{};
String[] booleanValue = new String[]{"true", "false"};
String[] hideStyles = Arrays.stream(HiddenChunkStyle.values()).map(HiddenChunkStyle::getValue)
.toArray(String[]::new);
String[] perspectives = MapManager.mapman.hdmapman.perspectives.keySet().toArray(new String[0]);
String[] shaders = MapManager.mapman.hdmapman.shaders.keySet().toArray(new String[0]);
String[] lightings = MapManager.mapman.hdmapman.lightings.keySet().toArray(new String[0]);
String[] imageFormats = Arrays.stream(MapType.ImageFormat.values())
.map(MapType.ImageFormat::getID).toArray(String[]::new);
Supplier<String[]> emptySupplier = () -> emptyValue;
Supplier<String[]> booleanSupplier = () -> booleanValue;
Supplier<String[]> hideStyleSupplier = () -> hideStyles;
Supplier<String[]> perspectiveSupplier = () -> perspectives;
Supplier<String[]> shaderSupplier = () -> shaders;
Supplier<String[]> lightingSupplier = () -> lightings;
Supplier<String[]> imageFormatSupplier = () -> imageFormats;
//Arguments for /dmap worldset
Map<String, Supplier<String[]>> worldSetArgs = new LinkedHashMap<>();
worldSetArgs.put("enabled", booleanSupplier);
worldSetArgs.put("title", emptySupplier);
worldSetArgs.put("order", emptySupplier);
worldSetArgs.put("center", emptySupplier);
worldSetArgs.put("sendposition", booleanSupplier);
worldSetArgs.put("sendhealth", booleanSupplier);
worldSetArgs.put("showborder", booleanSupplier);
worldSetArgs.put("protected", booleanSupplier);
worldSetArgs.put("extrazoomout", emptySupplier);
worldSetArgs.put("tileupdatedelay", emptySupplier);
//Arguments for /dmap worldaddlimit
Map<String, Supplier<String[]>> worldAddLimitArgs = new LinkedHashMap<>();
worldAddLimitArgs.put("type", () -> new String[]{"round", "rect"});
worldAddLimitArgs.put("limittype", () -> new String[]{"visible", "hidden"});
worldAddLimitArgs.put("style", hideStyleSupplier);
worldAddLimitArgs.put("corner1", emptySupplier);
worldAddLimitArgs.put("corner2", emptySupplier);
worldAddLimitArgs.put("center", emptySupplier);
worldAddLimitArgs.put("radius", emptySupplier);
//Arguments for /dmap mapadd/mapset
Map<String, Supplier<String[]>> mapSetArgs = new LinkedHashMap<>();
mapSetArgs.put("title", emptySupplier);
mapSetArgs.put("icon", emptySupplier);
mapSetArgs.put("order", emptySupplier);
mapSetArgs.put("prefix", emptySupplier);
mapSetArgs.put("perspective", perspectiveSupplier);
mapSetArgs.put("shader", shaderSupplier);
mapSetArgs.put("lighting", lightingSupplier);
mapSetArgs.put("img-format", imageFormatSupplier);
mapSetArgs.put("protected", booleanSupplier);
mapSetArgs.put("append-to-world", emptySupplier);
mapSetArgs.put("mapzoomin", emptySupplier);
mapSetArgs.put("mapzoomout", emptySupplier);
mapSetArgs.put("boostzoom", emptySupplier);
mapSetArgs.put("tileupdatedelay", emptySupplier);
tabCompletions = new HashMap<>();
tabCompletions.put("worldaddlimit", worldAddLimitArgs);
tabCompletions.put("worldset", worldSetArgs);
tabCompletions.put("mapset", mapSetArgs); //Also used for mapadd
}
private boolean checkIfActive(DynmapCore core, DynmapCommandSender sender) {
if ((!core.getPauseFullRadiusRenders()) || (!core.getPauseUpdateRenders())) {
@ -94,6 +169,78 @@ public class DynmapMapCommands {
}
return rslt;
}
public List<String> getTabCompletions(DynmapCommandSender sender, String[] args, DynmapCore core) {
/* Re-parse args - handle doublequotes */
args = DynmapCore.parseArgs(args, sender, true);
if (args == null || args.length <= 1) {
return Collections.emptyList();
}
if (tabCompletions == null) {
initTabCompletions();
}
String cmd = args[0];
if (cmd.equalsIgnoreCase("worldlist")
&& core.checkPlayerPermission(sender, "dmap.worldlist")) {
List<String> suggestions = core.getWorldSuggestions(args[args.length - 1]);
suggestions.removeAll(Arrays.asList(args)); //Remove suggestions present in other arguments
return suggestions;
} else if ((cmd.equalsIgnoreCase("maplist")
&& core.checkPlayerPermission(sender, "dmap.maplist"))
|| (cmd.equalsIgnoreCase("worldgetlimits")
&& core.checkPlayerPermission(sender, "dmap.worldlist"))) {
if (args.length == 2) {
return core.getWorldSuggestions(args[1]);
}
} else if (cmd.equalsIgnoreCase("worldremovelimit")
&& core.checkPlayerPermission(sender, "dmap.worldset")) {
if (args.length == 2) {
return core.getWorldSuggestions(args[1]);
}
} else if (cmd.equalsIgnoreCase("worldaddlimit")
&& core.checkPlayerPermission(sender, "dmap.worldset")) {
if (args.length == 2) {
return core.getWorldSuggestions(args[1]);
} else {
return core.getFieldValueSuggestions(args, tabCompletions.get("worldaddlimit"));
}
} else if (cmd.equalsIgnoreCase("worldset")
&& core.checkPlayerPermission(sender, "dmap.worldset")) {
if (args.length == 2) {
return core.getWorldSuggestions(args[1]);
} else {
return core.getFieldValueSuggestions(args, tabCompletions.get("worldset"));
}
} else if (cmd.equalsIgnoreCase("mapdelete")
&& core.checkPlayerPermission(sender, "dmap.mapdelete")) {
if (args.length == 2) {
return core.getMapSuggestions(args[1]);
}
} else if (cmd.equalsIgnoreCase("worldreset")
&& core.checkPlayerPermission(sender, "dmap.worldreset")) {
if (args.length == 2) {
return core.getWorldSuggestions(args[1]);
}
} else if (cmd.equalsIgnoreCase("mapset")
&& core.checkPlayerPermission(sender, "dmap.mapset")) {
if (args.length == 2) {
return core.getMapSuggestions(args[1]);
} else {
return core.getFieldValueSuggestions(args, tabCompletions.get("mapset"));
}
} else if (cmd.equalsIgnoreCase("mapadd")) {
if (args.length > 2) {
return core.getFieldValueSuggestions(args, tabCompletions.get("mapset"));
}
}
return Collections.emptyList();
}
private boolean handleWorldList(DynmapCommandSender sender, String[] args, DynmapCore core) {
if(!core.checkPlayerPermission(sender, "dmap.worldlist"))

View File

@ -1,7 +1,13 @@
package org.dynmap.exporter;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.dynmap.DynmapCore;
import org.dynmap.DynmapLocation;
@ -91,6 +97,53 @@ public class DynmapExpCommands {
return rslt;
}
public List<String> getTabCompletions(DynmapCommandSender sender, String[] args, DynmapCore core) {
/* Re-parse args - handle doublequotes */
args = DynmapCore.parseArgs(args, sender, true);
if (args == null || args.length <= 1) {
return Collections.emptyList();
}
String cmd = args[0];
if(cmd.equalsIgnoreCase("set")) {
List<String> keys = new ArrayList<>(
Arrays.asList("x0", "x1", "y0", "y1", "z0", "z1", "world", "shader", "byChunk",
"byBlockID", "byBlockIDData", "byTexture"));
if (args.length % 2 == 0) { // Args contain only complete key value argument pairs (plus subcommand)
// Remove previous used keys
for (int i = 1; i < args.length; i += 2) {
keys.remove(args[i]);
}
return keys;
} else { // Incomplete key value argument pair, suggest values
final String lastKey = args[args.length - 2];
final String lastValue = args[args.length - 1];
switch(lastKey) {
case "world":
return core.getWorldSuggestions(lastValue);
case "shader":
return MapManager.mapman.hdmapman.shaders.keySet().stream()
.filter(value -> value.startsWith(lastValue))
.collect(Collectors.toList());
case "byChunk":
case "byBlockID":
case "byBlockIDData":
case "byTexture":
return Stream.of("true", "false")
.filter(value -> value.startsWith(lastValue))
.collect(Collectors.toList());
}
}
}
return Collections.emptyList();
}
private boolean handleInfo(DynmapCommandSender sender, String[] args, ExportContext ctx, DynmapCore core) {
sender.sendMessage(String.format("Bounds: <%s,%s,%s> - <%s,%s,%s> on world '%s'", val(ctx.xmin), val(ctx.ymin), val(ctx.zmin),
val(ctx.xmax), val(ctx.ymax), val(ctx.zmax), ctx.world));

View File

@ -10,8 +10,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -49,6 +51,7 @@ import org.dynmap.markers.PolyLineMarker;
import org.dynmap.utils.BufferOutputStream;
import org.dynmap.web.Json;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
/**
* Implementation class for MarkerAPI - should not be called directly
@ -65,6 +68,8 @@ public class MarkerAPIImpl implements MarkerAPI, Event.Listener<DynmapWorld> {
private DynmapCore core;
static MarkerAPIImpl api;
private Map<String, Map<String, Supplier<String[]>>> tabCompletions = null;
/* Built-in icons */
private static final String[] builtin_icons = {
"anchor", "bank", "basket", "bed", "beer", "bighouse", "blueflag", "bomb", "bookshelf", "bricks", "bronzemedal", "bronzestar",
@ -420,6 +425,182 @@ public class MarkerAPIImpl implements MarkerAPI, Event.Listener<DynmapWorld> {
}
}, 0);
}
/**
* Generates a map of field:value argument tab completion suggestions for every /dmarker subcommand
* This is quite long as there are a lot of arguments to deal with, and don't have Java 9 map literals
*/
private void initTabCompletions() {
//Static values
String[] emptyValue = new String[]{};
String[] booleanValue = new String[]{"true", "false"};
String[] typeValue = new String[]{"icon", "area", "line", "circle"};
Supplier<String[]> emptySupplier = () -> emptyValue;
Supplier<String[]> booleanSupplier = () -> booleanValue;
//Dynamic values
Supplier<String[]> iconSupplier = () -> markericons.keySet().toArray(new String[0]);
Supplier<String[]> markerSetSupplier = () -> markersets.keySet().toArray(new String[0]);
Supplier<String[]> worldSupplier = () ->
core.mapManager.getWorlds().stream().map(DynmapWorld::getName).toArray(String[]::new);
//Arguments used in multiple commands
Map<String, Supplier<String[]>> labelArg = Collections.singletonMap("label", emptySupplier);
Map<String, Supplier<String[]>> idArg = Collections.singletonMap("id", emptySupplier);
Map<String, Supplier<String[]>> newLabelArg = Collections.singletonMap("newlabel", emptySupplier);
Map<String, Supplier<String[]>> markerSetArg = Collections.singletonMap("set", markerSetSupplier);
Map<String, Supplier<String[]>> newSetArg = Collections.singletonMap("newset", markerSetSupplier);
Map<String, Supplier<String[]>> fileArg = Collections.singletonMap("file", emptySupplier);
//Arguments used in commands taking a location
Map<String, Supplier<String[]>> locationArgs = new LinkedHashMap<>();
locationArgs.put("x", emptySupplier);
locationArgs.put("y", emptySupplier);
locationArgs.put("z", emptySupplier);
locationArgs.put("world", worldSupplier);
//Args shared with all add/update commands
Map<String, Supplier<String[]>> sharedArgs = new LinkedHashMap<>(labelArg);
sharedArgs.putAll(idArg);
//Args shared with all add/update commands affecting objects visible on the map
Map<String, Supplier<String[]>> mapObjectArgs = new LinkedHashMap<>(sharedArgs);
mapObjectArgs.put("minzoom", emptySupplier);
mapObjectArgs.put("maxzoom", emptySupplier);
//Args for marker set add/update commands
Map<String, Supplier<String[]>> setArgs = new LinkedHashMap<>(mapObjectArgs);
setArgs.put("prio", emptySupplier);
setArgs.put("hide", booleanSupplier);
setArgs.put("showlabel", booleanSupplier);
setArgs.put("deficon", iconSupplier);
//Args for marker add/update commands
Map<String, Supplier<String[]>> markerArgs = new LinkedHashMap<>(mapObjectArgs);
markerArgs.putAll(markerSetArg);
markerArgs.put("markup", booleanSupplier);
markerArgs.put("icon", iconSupplier);
markerArgs.putAll(locationArgs);
//Args for area/line/circle add/update commands
Map<String, Supplier<String[]>> shapeArgs = new LinkedHashMap<>(mapObjectArgs);
shapeArgs.putAll(markerSetArg);
shapeArgs.put("markup", booleanSupplier);
shapeArgs.put("weight", emptySupplier);
shapeArgs.put("color", emptySupplier);
shapeArgs.put("opacity", emptySupplier);
//Args for area/circle add/update commands
Map<String, Supplier<String[]>> filledShapeArgs = new LinkedHashMap<>(shapeArgs);
filledShapeArgs.put("fillcolor", emptySupplier);
filledShapeArgs.put("fillopacity", emptySupplier);
filledShapeArgs.put("greeting", emptySupplier);
filledShapeArgs.put("greetingsub", emptySupplier);
filledShapeArgs.put("farewell", emptySupplier);
filledShapeArgs.put("farewellsub", emptySupplier);
filledShapeArgs.put("boost", booleanSupplier);
filledShapeArgs.putAll(locationArgs);
//Args for area add/update commands
Map<String, Supplier<String[]>> areaArgs = new LinkedHashMap<>(filledShapeArgs);
areaArgs.put("ytop", emptySupplier);
areaArgs.put("ybottom", emptySupplier);
//Args for circle add/update commands
Map<String, Supplier<String[]>> circleArgs = new LinkedHashMap<>(filledShapeArgs);
circleArgs.put("radius", emptySupplier);
circleArgs.put("radiusx", emptySupplier);
circleArgs.put("radiusz", emptySupplier);
//Args for icon add/update commands
Map<String, Supplier<String[]>> iconArgs = new LinkedHashMap<>(sharedArgs);
iconArgs.putAll(fileArg);
//Args for updateset command
Map<String, Supplier<String[]>> updateSetArgs = new LinkedHashMap<>(setArgs);
updateSetArgs.putAll(newLabelArg);
//Args for update (marker) command
Map<String, Supplier<String[]>> updateMarkerArgs = new LinkedHashMap<>(markerArgs);
updateMarkerArgs.putAll(newLabelArg);
updateMarkerArgs.putAll(newSetArg);
//Args for updateline command
Map<String, Supplier<String[]>> updateLineArgs = new LinkedHashMap<>(shapeArgs);
updateLineArgs.putAll(newLabelArg);
updateLineArgs.putAll(newSetArg);
//Args for updatearea command
Map<String, Supplier<String[]>> updateAreaArgs = new LinkedHashMap<>(areaArgs);
updateAreaArgs.putAll(newLabelArg);
updateAreaArgs.putAll(newSetArg);
//Args for updatecircle command
Map<String, Supplier<String[]>> updateCircleArgs = new LinkedHashMap<>(circleArgs);
updateCircleArgs.putAll(newLabelArg);
updateCircleArgs.putAll(newSetArg);
//Args for updateicon command
Map<String, Supplier<String[]>> updateIconArgs = new LinkedHashMap<>(iconArgs);
updateIconArgs.putAll(newLabelArg);
//Args for movehere command
Map<String, Supplier<String[]>> moveHereArgs = new LinkedHashMap<>(sharedArgs);
moveHereArgs.putAll(markerSetArg);
//Args for marker/area/circle/line delete commands
Map<String, Supplier<String[]>> deleteArgs = new LinkedHashMap<>(sharedArgs);
deleteArgs.putAll(markerSetArg);
//Args for label/desc commands
Map<String, Supplier<String[]>> descArgs = new LinkedHashMap<>(sharedArgs);
descArgs.putAll(markerSetArg);
descArgs.put("type", () -> typeValue);
//Args for label/desc import commands
Map<String, Supplier<String[]>> importArgs = new LinkedHashMap<>(descArgs);
importArgs.putAll(fileArg);
//Args for appendesc command
Map<String, Supplier<String[]>> appendArgs = new LinkedHashMap<>(descArgs);
appendArgs.put("desc", emptySupplier);
tabCompletions = new HashMap<>();
tabCompletions.put("add", markerArgs);
tabCompletions.put("addicon", iconArgs);
tabCompletions.put("addarea", areaArgs);
tabCompletions.put("addline", shapeArgs); //No unique args
tabCompletions.put("addcircle", circleArgs);
tabCompletions.put("addset", setArgs);
tabCompletions.put("update", updateMarkerArgs);
tabCompletions.put("updateicon", updateIconArgs);
tabCompletions.put("updatearea", updateAreaArgs);
tabCompletions.put("updateline", updateLineArgs);
tabCompletions.put("updatecircle", updateCircleArgs);
tabCompletions.put("updateset", updateSetArgs);
tabCompletions.put("movehere", moveHereArgs);
tabCompletions.put("delete", deleteArgs);
tabCompletions.put("deleteicon", sharedArgs); //Doesn't have set: arg
tabCompletions.put("deletearea", deleteArgs);
tabCompletions.put("deleteline", deleteArgs);
tabCompletions.put("deletecircle", deleteArgs);
tabCompletions.put("deleteset", sharedArgs); //Doesn't have set: arg
tabCompletions.put("list", markerSetArg);
tabCompletions.put("listareas", markerSetArg);
tabCompletions.put("listlines", markerSetArg);
tabCompletions.put("listcircles", markerSetArg);
tabCompletions.put("getdesc", descArgs);
tabCompletions.put("importdesc", importArgs);
tabCompletions.put("resetdesc", descArgs);
tabCompletions.put("getlabel", descArgs);
tabCompletions.put("importlabel", importArgs);
tabCompletions.put("appenddesc", appendArgs);
}
public void scheduleWriteJob() {
core.getServer().scheduleServerTask(new DoFileWrites(), 20);
@ -1320,6 +1501,32 @@ public class MarkerAPIImpl implements MarkerAPI, Event.Listener<DynmapWorld> {
}
}
public List<String> getTabCompletions(DynmapCommandSender sender, String[] args, DynmapCore core) {
/* Re-parse args - handle doublequotes */
args = DynmapCore.parseArgs(args, sender, true);
if (args == null || args.length <= 1) {
return Collections.emptyList();
}
if (tabCompletions == null) {
initTabCompletions();
}
String cmd = args[0];
if (cmd.equals("addcorner") && core.checkPlayerPermission(sender, "marker.addarea")) {
if (args.length == 5) {
return core.getWorldSuggestions(args[4]);
}
} else if (core.checkPlayerPermission(sender, "marker." + cmd)
&& tabCompletions.containsKey(cmd)) {
return core.getFieldValueSuggestions(args, tabCompletions.get(cmd));
}
return Collections.emptyList();
}
private static boolean processAddMarker(DynmapCore plugin, DynmapCommandSender sender, String cmd, String commandLabel, String[] args, DynmapPlayer player) {
String id, setid, label, iconid, markup;
String x, y, z, world, normalized_world;

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@ -1103,6 +1104,21 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
return false;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) {
DynmapCommandSender dsender;
if(sender instanceof Player) {
dsender = new BukkitPlayer((Player)sender);
}
else {
dsender = new BukkitCommandSender(sender);
}
if (core != null)
return core.getTabCompletions(dsender, cmd.getName(), args);
else
return Collections.emptyList();
}
@Override
public final MarkerAPI getMarkerAPI() {