diff --git a/src/api/java/baritone/api/command/datatypes/ForBlockOptionalMeta.java b/src/api/java/baritone/api/command/datatypes/ForBlockOptionalMeta.java index 5b949cd63..4c5006c39 100644 --- a/src/api/java/baritone/api/command/datatypes/ForBlockOptionalMeta.java +++ b/src/api/java/baritone/api/command/datatypes/ForBlockOptionalMeta.java @@ -18,29 +18,14 @@ package baritone.api.command.datatypes; import baritone.api.command.exception.CommandException; -import baritone.api.command.helpers.TabCompleteHelper; import baritone.api.utils.BlockOptionalMeta; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.properties.Property; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; public enum ForBlockOptionalMeta implements IDatatypeFor { INSTANCE; - /** - * Matches (domain:)?name([(property=value)*])? but the input can be truncated at any position. - * domain and name are [a-z0-9_.-]+ and [a-z0-9/_.-]+ because that's what mc 1.13+ accepts. - * property and value use the same format as domain. - */ - // Good luck reading this. - private static Pattern PATTERN = Pattern.compile("(?:[a-z0-9_.-]+:)?(?:[a-z0-9/_.-]+(?:\\[(?:(?:[a-z0-9_.-]+=[a-z0-9_.-]+,)*(?:[a-z0-9_.-]+(?:=(?:[a-z0-9_.-]+(?:\\])?)?)?)?|\\])?)?)?"); - @Override public BlockOptionalMeta get(IDatatypeContext ctx) throws CommandException { return new BlockOptionalMeta(ctx.getConsumer().getString()); @@ -48,107 +33,6 @@ public BlockOptionalMeta get(IDatatypeContext ctx) throws CommandException { @Override public Stream tabComplete(IDatatypeContext ctx) throws CommandException { - String arg = ctx.getConsumer().peekString(); - - if (!PATTERN.matcher(arg).matches()) { - // Invalid format; we can't complete this. - ctx.getConsumer().getString(); - return Stream.empty(); - } - - if (arg.endsWith("]")) { - // We are already done. - ctx.getConsumer().getString(); - return Stream.empty(); - } - - if (!arg.contains("[")) { - // no properties so we are completing the block id - return ctx.getConsumer().tabCompleteDatatype(BlockById.INSTANCE); - } - - ctx.getConsumer().getString(); - - // destructuring assignment? Please? - String blockId, properties; - { - String[] parts = splitLast(arg, '['); - blockId = parts[0]; - properties = parts[1]; - } - - Block block = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(blockId)).orElse(null); - if (block == null) { - // This block doesn't exist so there's no properties to complete. - return Stream.empty(); - } - - String leadingProperties, lastProperty; - { - String[] parts = splitLast(properties, ','); - leadingProperties = parts[0]; - lastProperty = parts[1]; - } - - if (!lastProperty.contains("=")) { - // The last property-value pair doesn't have a value yet so we are completing its name - Set usedProps = Stream.of(leadingProperties.split(",")) - .map(pair -> pair.split("=")[0]) - .collect(Collectors.toSet()); - - String prefix = arg.substring(0, arg.length() - lastProperty.length()); - return new TabCompleteHelper() - .append( - block.getStateDefinition() - .getProperties() - .stream() - .map(Property::getName) - ) - .filter(prop -> !usedProps.contains(prop)) - .filterPrefix(lastProperty) - .sortAlphabetically() - .map(prop -> prefix + prop) - .stream(); - } - - String lastName, lastValue; - { - String[] parts = splitLast(lastProperty, '='); - lastName = parts[0]; - lastValue = parts[1]; - } - - // We are completing the value of a property - String prefix = arg.substring(0, arg.length() - lastValue.length()); - - Property property = block.getStateDefinition().getProperty(lastName); - if (property == null) { - // The property does not exist so there's no values to complete - return Stream.empty(); - } - - return new TabCompleteHelper() - .append(getValues(property)) - .filterPrefix(lastValue) - .sortAlphabetically() - .map(val -> prefix + val) - .stream(); - } - - /** - * Always returns exactly two strings. - * If the separator is not found the FIRST returned string is empty. - */ - private static String[] splitLast(String string, char chr) { - int idx = string.lastIndexOf(chr); - if (idx == -1) { - return new String[]{"", string}; - } - return new String[]{string.substring(0, idx), string.substring(idx + 1)}; - } - - // this shouldn't need to be a separate method? - private static > Stream getValues(Property property) { - return property.getPossibleValues().stream().map(property::getName); + return ForBlockOptionalMetaLookup.tabComplete(ctx, false); } } diff --git a/src/api/java/baritone/api/command/datatypes/ForBlockOptionalMetaLookup.java b/src/api/java/baritone/api/command/datatypes/ForBlockOptionalMetaLookup.java new file mode 100644 index 000000000..ca255c8f8 --- /dev/null +++ b/src/api/java/baritone/api/command/datatypes/ForBlockOptionalMetaLookup.java @@ -0,0 +1,194 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.api.command.datatypes; + +import baritone.api.command.exception.CommandException; +import baritone.api.command.helpers.TabCompleteHelper; +import baritone.api.utils.BlockOptionalMeta; +import baritone.api.utils.BlockOptionalMetaLookup; +import com.google.common.collect.ImmutableMap; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public enum ForBlockOptionalMetaLookup implements IDatatypeFor { + INSTANCE; + + /** + * Matches #?(domain:)?name([(property=value)*])? but the input can be truncated at any position. + * domain and name are [a-z0-9_.-]+ and [a-z0-9/_.-]+ because that's what mc 1.13+ accepts. + * property and value use the same format as domain. + */ + // Good luck reading this. + private static final Pattern PREFIX_PATTERN = Pattern.compile("#?(?:[a-z0-9_.-]+:)?(?:[a-z0-9/_.-]+(?:\\[(?:(?:[a-z0-9_.-]+=[a-z0-9_.-]+,)*(?:[a-z0-9_.-]+(?:=(?:[a-z0-9_.-]+(?:\\])?)?)?)?|\\])?)?)?"); + + // id(\[properties?\])? where id and properties are any text with at least one character + private static final Pattern FULL_PATTERN = Pattern.compile("^(?.+?)(?:\\[(?.+?)?\\])?$"); + + @Override + public BlockOptionalMetaLookup get(IDatatypeContext ctx) throws CommandException { + Matcher matcher = FULL_PATTERN.matcher(ctx.getConsumer().getString()); + + if (!matcher.find()) { + throw new IllegalArgumentException("invalid block/tag selector"); + } + + String props = Optional.ofNullable(matcher.group("properties")).orElse(""); + Map properties = props.equals("") ? Collections.emptyMap() : parseProperties(props); + + return new BlockOptionalMetaLookup(parseTagOrBlockId(matcher.group("id")) + .stream() + .filter(block -> properties.keySet().stream() + .allMatch(name -> Optional.of(block) + .map(b -> b.getStateDefinition().getProperty(name)) + .map(p -> p.getValue(properties.get(name))) + .isPresent() + ) + ) + .map(BuiltInRegistries.BLOCK::getKey) + .map(id -> new BlockOptionalMeta(id + "[" + props + "]")) + .toArray(BlockOptionalMeta[]::new) + ); + } + + @Override + public Stream tabComplete(IDatatypeContext ctx) throws CommandException { + return tabComplete(ctx, true); + } + + static Stream tabComplete(IDatatypeContext ctx, boolean allowTags) throws CommandException { + String arg = ctx.getConsumer().peekString(); + + if (!PREFIX_PATTERN.matcher(arg).matches()) { + // Invalid format; we can't complete this. + return Stream.empty(); + } + + if (arg.startsWith("#") && !allowTags) { + return Stream.empty(); + } + + if (arg.endsWith("]")) { + // We are already done. + return Stream.empty(); + } + + if (!arg.contains("[")) { + // no properties so we are completing the block or tag id + TabCompleteHelper helper = new TabCompleteHelper(); + if (arg.startsWith("#") || arg.equals("") && allowTags) { + helper.append(BuiltInRegistries.BLOCK.getTags() + .map(pair -> pair.getFirst().location().toString())) + .filterPrefixNamespaced(arg.equals("") ? "" : arg.substring(1)) + .sortAlphabetically() + .map(name -> "#" + name); + } + if (!arg.startsWith("#")) { + helper.append(ctx.getConsumer().tabCompleteDatatype(BlockById.INSTANCE)); + } + return helper.stream(); + } + + Set blocks = parseTagOrBlockId(splitLast(arg, '[')[0]); + + String properties = splitLast(arg, '[')[1]; + String previousProps = splitLast(properties, ',')[0]; + String lastProp = splitLast(properties, ',')[1]; + + if (!lastProp.contains("=")) { + // The last property-value pair doesn't have a value yet so we are completing its name + Set usedProps = Stream.of(previousProps.split(",")) + .map(pair -> pair.split("=")[0]) + .collect(Collectors.toSet()); + + return blocks.stream() + .flatMap(b -> b.getStateDefinition() + .getProperties() + .stream() + .map(p -> p.getName()) + ) + .filter(prop -> !usedProps.contains(prop)) + .filter(prop -> prop.startsWith(lastProp)) + .distinct() + .sorted(String.CASE_INSENSITIVE_ORDER) + .map(prop -> arg.substring(0, arg.length() - lastProp.length()) + prop); + } + + String lastName = splitLast(lastProp, '=')[0]; + String lastValue = splitLast(lastProp, '=')[1]; + + return blocks.stream() + .map(b -> b.getStateDefinition().getProperty(lastName)) + .filter(p -> p != null) + .flatMap(p -> p.getPossibleValues().stream().map(p::getName)) + .filter(v -> v.startsWith(lastValue)) + .distinct() + .map(v -> arg.substring(0, arg.length() - lastValue.length()) + v); + } + + private static Map parseProperties(String raw) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (String pair : raw.split(",")) { + String[] parts = pair.split("="); + if (parts.length != 2) { + throw new IllegalArgumentException(String.format("\"%s\" is not a valid property-value pair", pair)); + } + builder.put(parts[0], parts[1]); + } + return builder.build(); + } + + private static Set parseTagOrBlockId(String id) { + ResourceLocation loc = new ResourceLocation(id.replaceAll("^#", "")); + if (id.startsWith("#")) { + return BuiltInRegistries.BLOCK + .getTag(TagKey.create(Registries.BLOCK, loc)) + .orElseThrow(() -> new IllegalArgumentException("No tag " + id)) + .stream() + .map(h -> h.value()) + .collect(Collectors.toSet()); + } else { + return BuiltInRegistries.BLOCK.getOptional(loc) + .map(Set::of) + .orElseThrow(() -> new IllegalArgumentException("No block " + id)); + } + } + + /** + * Always returns exactly two strings. + * If the separator is not found the FIRST returned string is empty. + */ + private static String[] splitLast(String string, char chr) { + int idx = string.lastIndexOf(chr); + if (idx == -1) { + return new String[]{"", string}; + } + return new String[]{string.substring(0, idx), string.substring(idx + 1)}; + } +} diff --git a/src/main/java/baritone/command/defaults/MineCommand.java b/src/main/java/baritone/command/defaults/MineCommand.java index eb2c22c6d..b4e545e4f 100644 --- a/src/main/java/baritone/command/defaults/MineCommand.java +++ b/src/main/java/baritone/command/defaults/MineCommand.java @@ -21,8 +21,9 @@ import baritone.api.IBaritone; import baritone.api.command.Command; import baritone.api.command.argument.IArgConsumer; -import baritone.api.command.datatypes.ForBlockOptionalMeta; +import baritone.api.command.datatypes.ForBlockOptionalMetaLookup; import baritone.api.command.exception.CommandException; +import baritone.api.command.exception.CommandInvalidStateException; import baritone.api.utils.BlockOptionalMeta; import java.util.ArrayList; @@ -42,7 +43,10 @@ public void execute(String label, IArgConsumer args) throws CommandException { args.requireMin(1); List boms = new ArrayList<>(); while (args.hasAny()) { - boms.add(args.getDatatypeFor(ForBlockOptionalMeta.INSTANCE)); + boms.addAll(args.getDatatypeFor(ForBlockOptionalMetaLookup.INSTANCE).blocks()); + } + if (boms.size() == 0) { + throw new CommandInvalidStateException("No target blocks specified"); } BaritoneAPI.getProvider().getWorldScanner().repack(ctx); logDirect(String.format("Mining %s", boms.toString())); @@ -53,9 +57,9 @@ public void execute(String label, IArgConsumer args) throws CommandException { public Stream tabComplete(String label, IArgConsumer args) throws CommandException { args.getAsOrDefault(Integer.class, 0); while (args.has(2)) { - args.getDatatypeFor(ForBlockOptionalMeta.INSTANCE); + args.getDatatypeFor(ForBlockOptionalMetaLookup.INSTANCE); } - return args.tabCompleteDatatype(ForBlockOptionalMeta.INSTANCE); + return args.tabCompleteDatatype(ForBlockOptionalMetaLookup.INSTANCE); } @Override diff --git a/src/main/java/baritone/command/defaults/SelCommand.java b/src/main/java/baritone/command/defaults/SelCommand.java index e789dcd97..3b5d76a2e 100644 --- a/src/main/java/baritone/command/defaults/SelCommand.java +++ b/src/main/java/baritone/command/defaults/SelCommand.java @@ -23,6 +23,7 @@ import baritone.api.command.argument.IArgConsumer; import baritone.api.command.datatypes.ForAxis; import baritone.api.command.datatypes.ForBlockOptionalMeta; +import baritone.api.command.datatypes.ForBlockOptionalMetaLookup; import baritone.api.command.datatypes.ForDirection; import baritone.api.command.datatypes.RelativeBlockPos; import baritone.api.command.exception.CommandException; @@ -133,9 +134,12 @@ public void execute(String label, IArgConsumer args) throws CommandException { List replacesList = new ArrayList<>(); replacesList.add(type); while (args.has(2)) { - replacesList.add(args.getDatatypeFor(ForBlockOptionalMeta.INSTANCE)); + replacesList.addAll(args.getDatatypeFor(ForBlockOptionalMetaLookup.INSTANCE).blocks()); } type = args.getDatatypeFor(ForBlockOptionalMeta.INSTANCE); + if (replacesList.size() == 0) { + throw new CommandInvalidStateException("No target blocks specified"); + } replaces = new BlockOptionalMetaLookup(replacesList.toArray(new BlockOptionalMeta[0])); alignment = null; } else if (action == Action.CYLINDER || action == Action.HCYLINDER) {