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.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Scanner;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
@ -1117,6 +1120,11 @@ public class DynmapCore implements DynmapCommonAPI {
/* Parse argument strings : handle quoted strings */ /* Parse argument strings : handle quoted strings */
public static String[] parseArgs(String[] args, DynmapCommandSender snd) { 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>(); ArrayList<String> rslt = new ArrayList<String>();
/* Build command line, so we can parse our way - make sure there is trailing space */ /* Build command line, so we can parse our way - make sure there is trailing space */
String cmdline = ""; String cmdline = "";
@ -1146,9 +1154,15 @@ public class DynmapCore implements DynmapCommonAPI {
sb.append(c); sb.append(c);
} }
} }
if(inquote) { /* If still in quote, syntax error */ if(inquote) { // If still in quote
snd.sendMessage("Error: unclosed doublequote"); if(allowUnclosedQuotes) {
return null; 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()]); 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", "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", "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", "<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", "list", "List details of all markers."),
new CommandInfo("dmarker", "icons", "List details of all icons."), new CommandInfo("dmarker", "icons", "List details of all icons."),
new CommandInfo("dmarker", "addset", "<label>", "Add marker set with label <label>."), new CommandInfo("dmarker", "addset", "<label>", "Add marker set with label <label>."),
@ -1351,7 +1365,164 @@ public class DynmapCore implements DynmapCommonAPI {
} }
sender.sendMessage(subcmdlist); 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) { public boolean processCommand(DynmapCommandSender sender, String cmd, String commandLabel, String[] args) {
if (mapManager == null) { // Initialization faulure if (mapManager == null) { // Initialization faulure
sender.sendMessage("Dynmap failed to initialize properly: commands not available"); sender.sendMessage("Dynmap failed to initialize properly: commands not available");
@ -1729,6 +1900,155 @@ public class DynmapCore implements DynmapCommonAPI {
return true; 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) { public boolean checkPlayerPermission(DynmapCommandSender sender, String permission) {
if (!(sender instanceof DynmapPlayer) || sender.isOp()) { if (!(sender instanceof DynmapPlayer) || sender.isOp()) {
return true; return true;

View File

@ -1,11 +1,16 @@
package org.dynmap; package org.dynmap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.function.Supplier;
import org.dynmap.common.DynmapCommandSender; import org.dynmap.common.DynmapCommandSender;
import org.dynmap.common.DynmapPlayer; import org.dynmap.common.DynmapPlayer;
@ -22,6 +27,76 @@ import org.dynmap.utils.VisibilityLimit;
* Handler for world and map edit commands (via /dmap) * Handler for world and map edit commands (via /dmap)
*/ */
public class DynmapMapCommands { 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) { private boolean checkIfActive(DynmapCore core, DynmapCommandSender sender) {
if ((!core.getPauseFullRadiusRenders()) || (!core.getPauseUpdateRenders())) { if ((!core.getPauseFullRadiusRenders()) || (!core.getPauseUpdateRenders())) {
@ -94,6 +169,78 @@ public class DynmapMapCommands {
} }
return rslt; 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) { private boolean handleWorldList(DynmapCommandSender sender, String[] args, DynmapCore core) {
if(!core.checkPlayerPermission(sender, "dmap.worldlist")) if(!core.checkPlayerPermission(sender, "dmap.worldlist"))

View File

@ -1,7 +1,13 @@
package org.dynmap.exporter; package org.dynmap.exporter;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; 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.DynmapCore;
import org.dynmap.DynmapLocation; import org.dynmap.DynmapLocation;
@ -91,6 +97,53 @@ public class DynmapExpCommands {
return rslt; 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) { 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), 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)); 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.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -49,6 +51,7 @@ import org.dynmap.markers.PolyLineMarker;
import org.dynmap.utils.BufferOutputStream; import org.dynmap.utils.BufferOutputStream;
import org.dynmap.web.Json; import org.dynmap.web.Json;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
/** /**
* Implementation class for MarkerAPI - should not be called directly * Implementation class for MarkerAPI - should not be called directly
@ -65,6 +68,8 @@ public class MarkerAPIImpl implements MarkerAPI, Event.Listener<DynmapWorld> {
private DynmapCore core; private DynmapCore core;
static MarkerAPIImpl api; static MarkerAPIImpl api;
private Map<String, Map<String, Supplier<String[]>>> tabCompletions = null;
/* Built-in icons */ /* Built-in icons */
private static final String[] builtin_icons = { private static final String[] builtin_icons = {
"anchor", "bank", "basket", "bed", "beer", "bighouse", "blueflag", "bomb", "bookshelf", "bricks", "bronzemedal", "bronzestar", "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); }, 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() { public void scheduleWriteJob() {
core.getServer().scheduleServerTask(new DoFileWrites(), 20); 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) { private static boolean processAddMarker(DynmapCore plugin, DynmapCommandSender sender, String cmd, String commandLabel, String[] args, DynmapPlayer player) {
String id, setid, label, iconid, markup; String id, setid, label, iconid, markup;
String x, y, z, world, normalized_world; 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.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -1103,6 +1104,21 @@ public class DynmapPlugin extends JavaPlugin implements DynmapAPI {
return false; 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 @Override
public final MarkerAPI getMarkerAPI() { public final MarkerAPI getMarkerAPI() {