Improve parsing from registries.

If multiple namespaces are present, suggestions will first suggest a
namespace, then once a namespace is selected, keys within that namespace.

Starting an argument with ":" will instead search across all namespaces
for matching keys.
This commit is contained in:
wizjany 2019-05-27 11:12:46 -04:00
parent ab1e09fdaf
commit 5e857b3547
5 changed files with 123 additions and 79 deletions

View File

@ -20,8 +20,8 @@
package com.sk89q.worldedit.command.argument; package com.sk89q.worldedit.command.argument;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.sk89q.worldedit.command.util.SuggestionHelper;
import com.sk89q.worldedit.registry.Keyed; import com.sk89q.worldedit.registry.Keyed;
import com.sk89q.worldedit.registry.NamespacedRegistry;
import com.sk89q.worldedit.registry.Registry; import com.sk89q.worldedit.registry.Registry;
import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.Component;
import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.TextComponent;
@ -45,8 +45,7 @@ import org.enginehub.piston.inject.Key;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import static org.enginehub.piston.converter.SuggestionHelper.limitByPrefix;
public final class RegistryConverter<V extends Keyed> implements ArgumentConverter<V> { public final class RegistryConverter<V extends Keyed> implements ArgumentConverter<V> {
@ -85,12 +84,10 @@ public final class RegistryConverter<V extends Keyed> implements ArgumentConvert
private final Registry<V> registry; private final Registry<V> registry;
private final TextComponent choices; private final TextComponent choices;
private final boolean namespaced;
private RegistryConverter(Registry<V> registry) { private RegistryConverter(Registry<V> registry) {
this.registry = registry; this.registry = registry;
this.choices = TextComponent.of("any " + registry.getName()); this.choices = TextComponent.of("any " + registry.getName());
this.namespaced = registry instanceof NamespacedRegistry;
} }
@Override @Override
@ -109,9 +106,6 @@ public final class RegistryConverter<V extends Keyed> implements ArgumentConvert
@Override @Override
public List<String> getSuggestions(String input) { public List<String> getSuggestions(String input) {
if (namespaced && input.indexOf(':') < 0) { return SuggestionHelper.getRegistrySuggestions(registry, input).collect(Collectors.toList());
input = "minecraft:" + input;
}
return limitByPrefix(registry.keySet().stream(), input);
} }
} }

View File

@ -19,6 +19,9 @@
package com.sk89q.worldedit.command.util; package com.sk89q.worldedit.command.util;
import com.sk89q.worldedit.registry.Keyed;
import com.sk89q.worldedit.registry.NamespacedRegistry;
import com.sk89q.worldedit.registry.Registry;
import com.sk89q.worldedit.registry.state.Property; import com.sk89q.worldedit.registry.state.Property;
import com.sk89q.worldedit.world.block.BlockCategory; import com.sk89q.worldedit.world.block.BlockCategory;
import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockType;
@ -30,39 +33,32 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.enginehub.piston.converter.SuggestionHelper.byPrefix;
import static org.enginehub.piston.converter.SuggestionHelper.limitByPrefix;
/**
* Internal class for generating common command suggestions.
*/
public final class SuggestionHelper { public final class SuggestionHelper {
private SuggestionHelper() { private SuggestionHelper() {
} }
public static Stream<String> getBlockCategorySuggestions(String tag, boolean allowRandom) { public static Stream<String> getBlockCategorySuggestions(String tag, boolean allowRandom) {
final Stream<String> allTags = BlockCategory.REGISTRY.keySet().stream().map(str -> "##" + str); if (tag.isEmpty() || tag.equals("#")) {
if (tag.isEmpty()) { return Stream.of("##", "##*");
return allTags;
} }
if (tag.startsWith("#")) { if (tag.startsWith("#")) {
String key; if (tag.equals("##")) {
if (tag.startsWith("##")) { return Stream.concat(Stream.of("##*"), getNamespacedRegistrySuggestions(BlockCategory.REGISTRY, tag.substring(2)).map(s -> "##" + s));
key = tag.substring(2); } else if (tag.equals("##*") && allowRandom) {
if (key.isEmpty()) { return getNamespacedRegistrySuggestions(BlockCategory.REGISTRY, tag.substring(3)).map(s -> "##*" + s);
return allTags; } else {
} boolean wild = tag.startsWith("##*") && allowRandom;
boolean anyState = false; return getNamespacedRegistrySuggestions(BlockCategory.REGISTRY, tag.substring(wild ? 3 : 2)).map(s -> (wild ? "##*" : "##") + s);
if (allowRandom && key.charAt(0) == '*') {
key = key.substring(1);
anyState = true;
}
if (key.indexOf(':') < 0) {
key = "minecraft:" + key;
}
String finalTag = key.toLowerCase(Locale.ROOT);
final Stream<String> stream = BlockCategory.REGISTRY.keySet().stream().filter(s ->
s.startsWith(finalTag));
return anyState ? stream.map(s -> "##*" + s) : stream.map(s -> "##" + s);
} else if (tag.length() == 1) {
return allTags;
} }
} }
return Stream.empty(); return Stream.empty();
@ -103,7 +99,7 @@ public final class SuggestionHelper {
} else { } else {
Property<?> prop = propertyMap.get(matchProp); Property<?> prop = propertyMap.get(matchProp);
if (prop == null) { if (prop == null) {
return Stream.empty(); return propertyMap.keySet().stream().map(p -> lastValidInput + p);
} }
final List<String> values = prop.getValues().stream().map(v -> v.toString().toLowerCase(Locale.ROOT)).collect(Collectors.toList()); final List<String> values = prop.getValues().stream().map(v -> v.toString().toLowerCase(Locale.ROOT)).collect(Collectors.toList());
String matchVal = propVal[1].toLowerCase(Locale.ROOT); String matchVal = propVal[1].toLowerCase(Locale.ROOT);
@ -140,4 +136,38 @@ public final class SuggestionHelper {
} }
return Stream.empty(); return Stream.empty();
} }
public static <V extends Keyed> Stream<String> getRegistrySuggestions(Registry<V> registry, String input) {
if (registry instanceof NamespacedRegistry) {
return getNamespacedRegistrySuggestions(((NamespacedRegistry<V>) registry), input);
}
return limitByPrefix(registry.keySet().stream(), input).stream();
}
public static <V extends Keyed> Stream<String> getNamespacedRegistrySuggestions(NamespacedRegistry<V> registry, String input) {
if (input.isEmpty() || input.equals(":")) {
final Set<String> namespaces = registry.getKnownNamespaces();
if (namespaces.size() == 1) {
return registry.keySet().stream();
} else {
return namespaces.stream().map(s -> s + ":");
}
}
if (input.startsWith(":")) { // special case - search across namespaces
final String term = input.substring(1).toLowerCase(Locale.ROOT);
Predicate<String> search = byPrefix(term);
return registry.keySet().stream().filter(s -> search.test(s.substring(s.indexOf(':') + 1)));
}
// otherwise, we actually have some text to search
if (input.indexOf(':') < 0) {
// don't yet have namespace - search namespaces + default
final String lowerSearch = input.toLowerCase(Locale.ROOT);
String defKey = registry.getDefaultNamespace() + ":" + lowerSearch;
return Stream.concat(registry.keySet().stream().filter(s -> s.startsWith(defKey)),
registry.getKnownNamespaces().stream().filter(n -> n.startsWith(lowerSearch)).map(n -> n + ":"));
}
// have a namespace - search that
Predicate<String> search = byPrefix(input.toLowerCase(Locale.ROOT));
return registry.keySet().stream().filter(search);
}
} }

View File

@ -54,8 +54,6 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.enginehub.piston.converter.SuggestionHelper.limitByPrefix;
/** /**
* Parses block input strings. * Parses block input strings.
*/ */
@ -111,6 +109,7 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
* @param string Input string * @param string Input string
* @return Mapped string * @return Mapped string
*/ */
@SuppressWarnings("ConstantConditions")
private String woolMapper(String string) { private String woolMapper(String string) {
switch (string.toLowerCase(Locale.ROOT)) { switch (string.toLowerCase(Locale.ROOT)) {
case "white": case "white":
@ -191,7 +190,7 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
} catch (NoMatchException e) { } catch (NoMatchException e) {
throw e; // Pass-through throw e; // Pass-through
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); WorldEdit.logger.warn("Unknown state '" + parseableData + "'", e);
throw new NoMatchException("Unknown state '" + parseableData + "'"); throw new NoMatchException("Unknown state '" + parseableData + "'");
} }
} }
@ -204,14 +203,7 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
public Stream<String> getSuggestions(String input) { public Stream<String> getSuggestions(String input) {
final int idx = input.lastIndexOf('['); final int idx = input.lastIndexOf('[');
if (idx < 0) { if (idx < 0) {
if (input.indexOf(':') == -1) { return SuggestionHelper.getNamespacedRegistrySuggestions(BlockType.REGISTRY, input);
String key = ("minecraft:" + input).toLowerCase(Locale.ROOT);
return BlockType.REGISTRY.keySet().stream().filter(s -> s.startsWith(key));
}
if (input.contains(",")) {
return Stream.empty();
}
return limitByPrefix(BlockType.REGISTRY.keySet().stream(), input).stream();
} }
String blockType = input.substring(0, idx); String blockType = input.substring(0, idx);
BlockType type = BlockTypes.get(blockType.toLowerCase(Locale.ROOT)); BlockType type = BlockTypes.get(blockType.toLowerCase(Locale.ROOT));
@ -238,8 +230,10 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
// Legacy matcher // Legacy matcher
if (context.isTryingLegacy()) { if (context.isTryingLegacy()) {
try { try {
String[] split = blockAndExtraData[0].split(":"); String[] split = blockAndExtraData[0].split(":", 2);
if (split.length == 1) { if (split.length == 0) {
throw new InputParseException("Invalid colon.");
} else if (split.length == 1) {
state = LegacyMapper.getInstance().getBlockFromLegacy(Integer.parseInt(split[0])); state = LegacyMapper.getInstance().getBlockFromLegacy(Integer.parseInt(split[0]));
} else { } else {
state = LegacyMapper.getInstance().getBlockFromLegacy(Integer.parseInt(split[0]), Integer.parseInt(split[1])); state = LegacyMapper.getInstance().getBlockFromLegacy(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
@ -247,7 +241,7 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
if (state != null) { if (state != null) {
blockType = state.getBlockType(); blockType = state.getBlockType();
} }
} catch (NumberFormatException e) { } catch (NumberFormatException ignored) {
} }
} }
@ -310,23 +304,15 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
} else { } else {
// Attempt to lookup a block from ID or name. // Attempt to lookup a block from ID or name.
blockType = BlockTypes.get(typeString.toLowerCase(Locale.ROOT)); blockType = BlockTypes.get(typeString.toLowerCase(Locale.ROOT));
}
if (blockType == null) { if (blockType == null) {
throw new NoMatchException("Does not match a valid block type: '" + input + "'"); throw new NoMatchException("Does not match a valid block type: '" + input + "'");
}
} }
blockStates.putAll(parseProperties(blockType, stateProperties, context)); blockStates.putAll(parseProperties(blockType, stateProperties, context));
if (!context.isPreferringWildcard()) { if (context.isPreferringWildcard()) {
// No wildcards allowed => eliminate them. (Start with default state)
state = blockType.getDefaultState();
for (Map.Entry<Property<?>, Object> blockState : blockStates.entrySet()) {
@SuppressWarnings("unchecked")
Property<Object> objProp = (Property<Object>) blockState.getKey();
state = state.with(objProp, blockState.getValue());
}
} else {
FuzzyBlockState.Builder fuzzyBuilder = FuzzyBlockState.builder(); FuzzyBlockState.Builder fuzzyBuilder = FuzzyBlockState.builder();
fuzzyBuilder.type(blockType); fuzzyBuilder.type(blockType);
for (Map.Entry<Property<?>, Object> blockState : blockStates.entrySet()) { for (Map.Entry<Property<?>, Object> blockState : blockStates.entrySet()) {
@ -335,8 +321,20 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
fuzzyBuilder.withProperty(objProp, blockState.getValue()); fuzzyBuilder.withProperty(objProp, blockState.getValue());
} }
state = fuzzyBuilder.build(); state = fuzzyBuilder.build();
} else {
// No wildcards allowed => eliminate them. (Start with default state)
state = blockType.getDefaultState();
for (Map.Entry<Property<?>, Object> blockState : blockStates.entrySet()) {
@SuppressWarnings("unchecked")
Property<Object> objProp = (Property<Object>) blockState.getKey();
state = state.with(objProp, blockState.getValue());
}
} }
} }
// this should be impossible but IntelliJ isn't that smart
if (blockType == null) {
throw new NoMatchException("Does not match a valid block type: '" + input + "'");
}
// Check if the item is allowed // Check if the item is allowed
if (context.isRestricted()) { if (context.isRestricted()) {
@ -369,6 +367,7 @@ public class DefaultBlockParser extends InputParser<BaseBlock> {
} }
return new MobSpawnerBlock(state, mobName); return new MobSpawnerBlock(state, mobName);
} else { } else {
//noinspection ConstantConditions
return new MobSpawnerBlock(state, EntityTypes.PIG.getId()); return new MobSpawnerBlock(state, EntityTypes.PIG.getId());
} }
} else if (blockType == BlockTypes.PLAYER_HEAD || blockType == BlockTypes.PLAYER_WALL_HEAD) { } else if (blockType == BlockTypes.PLAYER_HEAD || blockType == BlockTypes.PLAYER_WALL_HEAD) {

View File

@ -21,22 +21,20 @@ package com.sk89q.worldedit.extension.factory.parser.mask;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.command.util.SuggestionHelper;
import com.sk89q.worldedit.extension.input.InputParseException; import com.sk89q.worldedit.extension.input.InputParseException;
import com.sk89q.worldedit.extension.input.ParserContext; import com.sk89q.worldedit.extension.input.ParserContext;
import com.sk89q.worldedit.extension.platform.Capability;
import com.sk89q.worldedit.function.mask.BiomeMask2D; import com.sk89q.worldedit.function.mask.BiomeMask2D;
import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.function.mask.Mask;
import com.sk89q.worldedit.function.mask.Masks; import com.sk89q.worldedit.function.mask.Masks;
import com.sk89q.worldedit.internal.registry.InputParser; import com.sk89q.worldedit.internal.registry.InputParser;
import com.sk89q.worldedit.session.request.RequestExtent; import com.sk89q.worldedit.session.request.RequestExtent;
import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.biome.BiomeType;
import com.sk89q.worldedit.world.biome.Biomes;
import com.sk89q.worldedit.world.registry.BiomeRegistry;
import java.util.Collection; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
public class BiomeMaskParser extends InputParser<Mask> { public class BiomeMaskParser extends InputParser<Mask> {
@ -47,20 +45,20 @@ public class BiomeMaskParser extends InputParser<Mask> {
@Override @Override
public Stream<String> getSuggestions(String input) { public Stream<String> getSuggestions(String input) {
final Stream<String> allBiomes = BiomeType.REGISTRY.keySet().stream().map(biomeType -> "$" + biomeType);
if (input.isEmpty()) { if (input.isEmpty()) {
return allBiomes; return Stream.of("$");
} }
if (input.charAt(0) == '$') { if (input.charAt(0) == '$') {
String key = input.substring(1); input = input.substring(1);
if (key.isEmpty()) { final int lastTermIdx = input.lastIndexOf(',');
return allBiomes; if (lastTermIdx <= 0) {
return SuggestionHelper.getNamespacedRegistrySuggestions(BiomeType.REGISTRY, input).map(s -> "$" + s);
} }
if (key.indexOf(':') < 0) { String prev = input.substring(0, lastTermIdx) + ",";
key = "minecraft:" + key; Set<String> prevBiomes = Arrays.stream(prev.split(",", 0)).collect(Collectors.toSet());
} String search = input.substring(lastTermIdx + 1);
String biomeId = key.toLowerCase(Locale.ROOT); return SuggestionHelper.getNamespacedRegistrySuggestions(BiomeType.REGISTRY, search)
return BiomeType.REGISTRY.keySet().stream().filter(s -> s.startsWith(biomeId)).map(s -> "$" + s); .filter(s -> !prevBiomes.contains(s)).map(s -> "$" + prev + s);
} }
return Stream.empty(); return Stream.empty();
} }
@ -72,10 +70,8 @@ public class BiomeMaskParser extends InputParser<Mask> {
} }
Set<BiomeType> biomes = new HashSet<>(); Set<BiomeType> biomes = new HashSet<>();
BiomeRegistry biomeRegistry = worldEdit.getPlatformManager().queryCapability(Capability.GAME_HOOKS).getRegistries().getBiomeRegistry();
Collection<BiomeType> knownBiomes = BiomeType.REGISTRY.values();
for (String biomeName : Splitter.on(",").split(input.substring(1))) { for (String biomeName : Splitter.on(",").split(input.substring(1))) {
BiomeType biome = Biomes.findBiomeByName(knownBiomes, biomeName, biomeRegistry); BiomeType biome = BiomeType.REGISTRY.get(biomeName);
if (biome == null) { if (biome == null) {
throw new InputParseException("Unknown biome '" + biomeName + '\''); throw new InputParseException("Unknown biome '" + biomeName + '\'');
} }

View File

@ -23,9 +23,13 @@ import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public final class NamespacedRegistry<V extends Keyed> extends Registry<V> { public final class NamespacedRegistry<V extends Keyed> extends Registry<V> {
private static final String MINECRAFT_NAMESPACE = "minecraft"; private static final String MINECRAFT_NAMESPACE = "minecraft";
private final Set<String> knownNamespaces = new HashSet<>();
private final String defaultNamespace; private final String defaultNamespace;
public NamespacedRegistry(final String name) { public NamespacedRegistry(final String name) {
@ -46,8 +50,29 @@ public final class NamespacedRegistry<V extends Keyed> extends Registry<V> {
@Override @Override
public V register(final String key, final V value) { public V register(final String key, final V value) {
requireNonNull(key, "key"); requireNonNull(key, "key");
checkState(key.indexOf(':') > -1, "key is not namespaced"); final int i = key.indexOf(':');
return super.register(key, value); checkState(i > 0, "key is not namespaced");
final V registered = super.register(key, value);
knownNamespaces.add(key.substring(0, i));
return registered;
}
/**
* Get a set of the namespaces of all registered keys.
*
* @return set of namespaces
*/
public Set<String> getKnownNamespaces() {
return Collections.unmodifiableSet(knownNamespaces);
}
/**
* Get the default namespace for this registry.
*
* @return the default namespace
*/
public String getDefaultNamespace() {
return defaultNamespace;
} }
private String orDefaultNamespace(final String key) { private String orDefaultNamespace(final String key) {