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
This commit is contained in:
Hannes Greule 2020-07-01 14:01:39 +02:00 committed by GitHub
parent 6be429cc96
commit 0bb6bc3563
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 253 additions and 7 deletions

View File

@ -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<String> 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<String> 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;
}
}

View File

@ -58,8 +58,8 @@ public class BlockFactory extends AbstractFactory<BaseBlock> {
*/
public Set<BaseBlock> parseFromListInput(String input, ParserContext context) throws InputParseException {
Set<BaseBlock> 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;

View File

@ -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<Pattern> {
register(new TypeOrStateApplyingPatternParser(worldEdit));
register(new RandomStatePatternParser(worldEdit));
register(new BlockCategoryPatternParser(worldEdit));
// FAWE
register(new SimplexPatternParser(worldEdit));
}
}

View File

@ -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 <E> the parse result.
*/
public abstract class RichParser<E> extends InputParser<E> {
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<String> 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 "<required>"
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<String> 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<String> 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]);
}
}

View File

@ -38,8 +38,9 @@ public class RandomPatternParser extends InputParser<Pattern> {
@Override
public Stream<String> getSuggestions(String input) {
String[] splits = input.split(",", -1);
List<String> patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true);
List<String> patterns = StringUtil.split(input, ',', '[', ']');
/*String[] splits = input.split(",", -1);
List<String> patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true);*/
if (patterns.size() == 1) {
return Stream.empty();
}
@ -63,8 +64,9 @@ public class RandomPatternParser extends InputParser<Pattern> {
public Pattern parseFromInput(String input, ParserContext context) throws InputParseException {
RandomPattern randomPattern = new RandomPattern();
String[] splits = input.split(",", -1);
List<String> patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true);
List<String> patterns = StringUtil.split(input, ',', '[', ']');
/*String[] splits = input.split(",", -1);
List<String> 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<Pattern> {
// 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 + "'");

View File

@ -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<Pattern> {
private static final String SIMPLEX_PREFIX = "#simplex";
public SimplexPatternParser(WorldEdit worldEdit) {
super(worldEdit, SIMPLEX_PREFIX);
}
@Override
protected Stream<String> 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<String> 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;
}
}

View File

@ -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.
*