From 0bb6bc3563526a56d26f7640bb1b22a16ccf70ce Mon Sep 17 00:00:00 2001 From: Hannes Greule Date: Wed, 1 Jul 2020 14:01:39 +0200 Subject: [PATCH] Start reimplementation of simplex pattern (#520) * Start reimplementation of simplex pattern * Fix suggestions * Allow nested weighted patterns * Add documentation and improve error handling * Remove unnecessary code and obsolete TODOs --- .../main/java/com/sk89q/util/StringUtil.java | 34 +++++ .../extension/factory/BlockFactory.java | 4 +- .../extension/factory/PatternFactory.java | 4 + .../extension/factory/parser/RichParser.java | 118 ++++++++++++++++++ .../parser/pattern/RandomPatternParser.java | 12 +- .../parser/pattern/SimplexPatternParser.java | 75 +++++++++++ .../function/pattern/RandomPattern.java | 13 ++ 7 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/RichParser.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/SimplexPatternParser.java diff --git a/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java b/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java index 1aceda930..8231d3aa8 100644 --- a/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java +++ b/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java @@ -20,6 +20,7 @@ package com.sk89q.util; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -331,4 +332,37 @@ public final class StringUtil { return parsableBlocks; } + + /** + * Splits a string respecting enclosing quotes. + * + * @param input the input to split. + * @param delimiter the delimiter to split on. + * @param open the opening quote character. + * @param close the closing quote character. + * @return a list of split strings. + */ + public static List split(String input, char delimiter, char open, char close) { + if (input.indexOf(open) == -1 && input.indexOf(close) == -1) { + return Arrays.asList(input.split(String.valueOf(delimiter))); + } + int level = 0; + int begin = 0; + List split = new ArrayList<>(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c == delimiter && level == 0) { + split.add(input.substring(begin, i)); + begin = i + 1; + } else if (c == open) { + level++; + } else if (c == close) { + level--; + } + } + if (begin < input.length()) { + split.add(input.substring(begin)); + } + return split; + } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/BlockFactory.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/BlockFactory.java index 4f5a4fdbc..693cd9b8e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/BlockFactory.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/BlockFactory.java @@ -58,8 +58,8 @@ public class BlockFactory extends AbstractFactory { */ public Set parseFromListInput(String input, ParserContext context) throws InputParseException { Set blocks = new HashSet<>(); - String[] splits = input.split(","); - for (String token : StringUtil.parseListInQuotes(splits, ',', '[', ']', true)) { + // String[] splits = input.split(","); + for (String token : StringUtil.split(input, ',', '[', ']')) { blocks.add(parseFromInput(token, context)); } return blocks; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java index 014229a3e..41bece423 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java @@ -24,6 +24,7 @@ import com.sk89q.worldedit.extension.factory.parser.pattern.BlockCategoryPattern import com.sk89q.worldedit.extension.factory.parser.pattern.ClipboardPatternParser; import com.sk89q.worldedit.extension.factory.parser.pattern.RandomPatternParser; import com.sk89q.worldedit.extension.factory.parser.pattern.RandomStatePatternParser; +import com.sk89q.worldedit.extension.factory.parser.pattern.SimplexPatternParser; import com.sk89q.worldedit.extension.factory.parser.pattern.SingleBlockPatternParser; import com.sk89q.worldedit.extension.factory.parser.pattern.TypeOrStateApplyingPatternParser; import com.sk89q.worldedit.function.pattern.Pattern; @@ -54,6 +55,9 @@ public final class PatternFactory extends AbstractFactory { register(new TypeOrStateApplyingPatternParser(worldEdit)); register(new RandomStatePatternParser(worldEdit)); register(new BlockCategoryPatternParser(worldEdit)); + + // FAWE + register(new SimplexPatternParser(worldEdit)); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/RichParser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/RichParser.java new file mode 100644 index 000000000..6bd9261fa --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/RichParser.java @@ -0,0 +1,118 @@ +package com.sk89q.worldedit.extension.factory.parser; + +import com.sk89q.util.StringUtil; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.internal.registry.InputParser; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import java.util.stream.Stream; + +/** + * A rich parser allows parsing of patterns and masks with extra arguments, + * e.g. #simplex[scale][pattern]. + * + * @param the parse result. + */ +public abstract class RichParser extends InputParser { + private final String prefix; + private final String required; + + /** + * Create a new rich parser with a defined prefix for the result, e.g. {@code #simplex}. + * + * @param worldEdit the worldedit instance. + * @param prefix the prefix of this parser result. + */ + protected RichParser(WorldEdit worldEdit, String prefix) { + super(worldEdit); + this.prefix = prefix; + this.required = prefix + "["; + } + + @Override + public Stream getSuggestions(String input) { + // we don't even want to start suggesting if it's not meant to be this parser result + if (input.length() > this.required.length() && !input.startsWith(this.required)) { + return Stream.empty(); + } + // suggest until the first [ as long as it isn't fully typed + if (input.length() < this.required.length()) { + return Stream.of(this.required).filter(s -> s.startsWith(input)); + } + // we know that it is at least "" + String[] strings = extractArguments(input.substring(this.prefix.length()), false); + StringJoiner joiner = new StringJoiner(","); + for (int i = 0; i < strings.length - 1; i++) { + joiner.add("[" + strings[i] + "]"); + } + String previous = this.prefix + joiner; + return getSuggestions(strings[strings.length - 1], strings.length - 1).map(s -> previous + "[" + s + "]"); + } + + @Override + public E parseFromInput(String input, ParserContext context) throws InputParseException { + if (!input.startsWith(this.prefix)) return null; + if (input.length() < this.prefix.length()) return null; + String[] arguments = extractArguments(input.substring(prefix.length()), true); + return parseFromInput(arguments, context); + } + + /** + * Returns a stream of suggestions for the argument at the given index. + * + * @param argumentInput the already provided input for the argument at the given index. + * @param index the index of the argument to get suggestions for. + * @return a stream of suggestions matching the given input for the argument at the given index. + */ + protected abstract Stream getSuggestions(String argumentInput, int index); + + /** + * Parses the already split arguments. + * + * @param arguments the array of arguments that were split (can be empty). + * @param context the context of this parsing process. + * @return the resulting parsed type. + * @throws InputParseException if the input couldn't be parsed correctly. + */ + protected abstract E parseFromInput(@NotNull String[] arguments, ParserContext context) throws InputParseException; + + /** + * Extracts arguments enclosed by {@code []} into an array. + * Example: {@code [Hello][World]} results in a list containing {@code Hello} and {@code World}. + * + * @param input the input to extract arguments from. + * @param requireClosing whether or not the extraction requires valid bracketing. + * @return an array of extracted arguments. + * @throws InputParseException if {@code requireClosing == true} and the count of [ != the count of ] + */ + protected String[] extractArguments(String input, boolean requireClosing) throws InputParseException { + int open = 0; // the "level" + int openIndex = 0; + int i = 0; + List arguments = new ArrayList<>(); + for (; i < input.length(); i++) { + if (input.charAt(i) == '[') { + if (open++ == 0) { + openIndex = i; + } + } + if (input.charAt(i) == ']') { + if (--open == 0) { + arguments.add(input.substring(openIndex + 1, i)); + } + } + } + if (!requireClosing && open > 0) { + arguments.add(input.substring(openIndex + 1)); + } + if (requireClosing && open != 0) { + throw new InputParseException("Invalid bracketing, are you missing a '[' or ']'?"); + } + return arguments.toArray(new String[0]); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/RandomPatternParser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/RandomPatternParser.java index 81b1b11e3..66c55be3e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/RandomPatternParser.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/RandomPatternParser.java @@ -38,8 +38,9 @@ public class RandomPatternParser extends InputParser { @Override public Stream getSuggestions(String input) { - String[] splits = input.split(",", -1); - List patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true); + List patterns = StringUtil.split(input, ',', '[', ']'); + /*String[] splits = input.split(",", -1); + List patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true);*/ if (patterns.size() == 1) { return Stream.empty(); } @@ -63,8 +64,9 @@ public class RandomPatternParser extends InputParser { public Pattern parseFromInput(String input, ParserContext context) throws InputParseException { RandomPattern randomPattern = new RandomPattern(); - String[] splits = input.split(",", -1); - List patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true); + List patterns = StringUtil.split(input, ',', '[', ']'); + /*String[] splits = input.split(",", -1); + List patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true);*/ if (patterns.size() == 1) { return null; // let a 'single'-pattern parser handle it } @@ -74,7 +76,7 @@ public class RandomPatternParser extends InputParser { // Parse special percentage syntax if (token.matches("[0-9]+(\\.[0-9]*)?%.*")) { - String[] p = token.split("%"); + String[] p = token.split("%", 2); if (p.length < 2) { throw new InputParseException("Missing the type after the % symbol for '" + input + "'"); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/SimplexPatternParser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/SimplexPatternParser.java new file mode 100644 index 000000000..547f96390 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/SimplexPatternParser.java @@ -0,0 +1,75 @@ +package com.sk89q.worldedit.extension.factory.parser.pattern; + +import com.boydti.fawe.object.random.SimplexRandom; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.factory.parser.RichParser; +import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.function.pattern.Pattern; +import com.sk89q.worldedit.function.pattern.RandomPattern; +import com.sk89q.worldedit.world.block.BlockStateHolder; +import org.jetbrains.annotations.NotNull; + +import java.util.stream.Stream; + +public class SimplexPatternParser extends RichParser { + private static final String SIMPLEX_PREFIX = "#simplex"; + + public SimplexPatternParser(WorldEdit worldEdit) { + super(worldEdit, SIMPLEX_PREFIX); + } + + @Override + protected Stream getSuggestions(String argumentInput, int index) { + if (index == 0) { + if (argumentInput.isEmpty()) { + return Stream.of("1", "2", "3", "4", "5", "6", "7", "8", "9"); + } + // if already a valid number, suggest more digits + if (isDouble(argumentInput)) { + Stream numbers = Stream.of("", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"); + if (argumentInput.indexOf('.') == -1) { + numbers = Stream.concat(numbers, Stream.of(".")); + } + return numbers.map(s -> argumentInput + s); + } + // no valid input anymore + return Stream.empty(); + } + if (index == 1) { + return worldEdit.getPatternFactory().getSuggestions(argumentInput).stream(); + } + return Stream.empty(); + } + + @Override + protected Pattern parseFromInput(@NotNull String[] arguments, ParserContext context) { + if (arguments.length != 2) { + throw new InputParseException("Simplex requires a scale and a pattern, e.g. #simplex[5][dirt,stone]"); + } + double scale = Double.parseDouble(arguments[0]); + scale = 1d / Math.max(1, scale); + Pattern inner = worldEdit.getPatternFactory().parseFromInput(arguments[1], context); + if (inner instanceof RandomPattern) { + return new RandomPattern(new SimplexRandom(scale), (RandomPattern) inner); + } else if (inner instanceof BlockStateHolder) { + return inner; // single blocks won't have any impact on how simplex behaves + } else { + throw new InputParseException("Pattern " + inner.getClass().getSimpleName() + " cannot be used with #simplex"); + } + } + + private static boolean isDouble(String input) { + boolean point = false; + for (char c : input.toCharArray()) { + if (!Character.isDigit(c)) { + if (c == '.' && !point) { + point = true; + } else { + return false; + } + } + } + return true; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/RandomPattern.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/RandomPattern.java index d5ecc2b44..3e02cdfaa 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/RandomPattern.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/RandomPattern.java @@ -52,6 +52,19 @@ public class RandomPattern extends AbstractPattern { this.random = random; } + /** + * Create a random pattern from an existing one but with a different random. + * + * @param random the new random to use. + * @param parent the existing random pattern. + */ + public RandomPattern(SimpleRandom random, RandomPattern parent) { + this.random = random; + this.weights = parent.weights; + this.collection = RandomCollection.of(weights, random); + this.patterns = parent.patterns; + } + /** * Add a pattern to the weight list of patterns. *